From 4cde58b3e23f74eab6ecd975a6a794040ac7dc4a Mon Sep 17 00:00:00 2001 From: Toastie Date: Wed, 20 Dec 2023 20:56:08 +1300 Subject: [PATCH] Full system rewrite incoming --- .../Abstractions/creds/IBotCredentials.cs | 78 - .../Abstractions/creds/IBotCredsProvider.cs | 8 - .../Abstractions/strings/CommandStrings.cs | 13 - .../Abstractions/strings/IBotStrings.cs | 16 - .../strings/IBotStringsExtensions.cs | 17 - .../strings/IBotStringsProvider.cs | 28 - .../Abstractions/strings/IStringSource.cs | 17 - .../Abstractions/strings/LocStr.cs | 13 - .../Attributes/AliasesAttribute.cs | 12 - .../Attributes/CmdAttribute.cs | 18 - .../Attributes/DIIgnoreAttribute.cs | 11 - .../Attributes/EllieOptionsAttribute.cs | 7 - .../Attributes/NoPublicBotAttribute.cs | 21 - .../Attributes/OnlyPublicBotAttribute.cs | 21 - .../Attributes/OwnerOnlyAttribute.cs | 19 - .../Attributes/RatelimitAttribute.cs | 38 - .../Attributes/UserPermAttribute.cs | 29 - src/Ellie.Bot.Common/BotCommandTypeReader.cs | 30 - src/Ellie.Bot.Common/CleanupModuleBase.cs | 25 - src/Ellie.Bot.Common/CleaverBotResponseStr.cs | 10 - src/Ellie.Bot.Common/CommandNameLoadHelper.cs | 31 - src/Ellie.Bot.Common/Configs/BotConfig.cs | 203 --- src/Ellie.Bot.Common/Configs/IConfigSeria.cs | 18 - src/Ellie.Bot.Common/Creds.cs | 272 ---- src/Ellie.Bot.Common/Currency/CurrencyType.cs | 5 - src/Ellie.Bot.Common/Currency/IBankService.cs | 10 - .../Currency/ICurrencyService.cs | 40 - src/Ellie.Bot.Common/Currency/ITxTracker.cs | 9 - src/Ellie.Bot.Common/Currency/IWallet.cs | 40 - src/Ellie.Bot.Common/Currency/TxData.cs | 7 - src/Ellie.Bot.Common/DbService.cs | 18 - src/Ellie.Bot.Common/DoAsUserMessage.cs | 141 -- src/Ellie.Bot.Common/Ellie.Bot.Common.csproj | 5 +- src/Ellie.Bot.Common/EllieModule.cs | 143 -- src/Ellie.Bot.Common/EllieTypeReader.cs | 15 - .../Extensions/DbExtensions.cs | 13 - .../Extensions/ImagesharpExtensions.cs | 97 -- src/Ellie.Bot.Common/GlobalUsings.cs | 31 - src/Ellie.Bot.Common/IBot.cs | 12 - src/Ellie.Bot.Common/ICloneable.cs | 8 - src/Ellie.Bot.Common/ICurrencyProvider.cs | 29 - .../IDiscordPermOverrideService.cs | 7 - src/Ellie.Bot.Common/IEllieCommandOptions.cs | 7 - src/Ellie.Bot.Common/ILogCommandService.cs | 35 - src/Ellie.Bot.Common/IPermissionChecker.cs | 13 - src/Ellie.Bot.Common/IPlaceholderProvider.cs | 7 - .../Interaction/EllieInteraction.cs | 82 - .../Interaction/EllieInteractionData.cs | 8 - .../Interaction/EllieInteractionService.cs | 20 - .../Interaction/IEllieInteractionService.cs | 8 - .../Interaction/SimpleInteraction.cs | 20 - .../Marmalade/IMarmaladeLoaderService.cs | 24 - .../Marmalade/MarmaladeLoadResult.cs | 10 - .../Marmalade/MarmaladeUnloadResult.cs | 9 - src/Ellie.Bot.Common/MessageType.cs | 8 - .../ModuleBehaviors/IBehavior.cs | 6 - .../ModuleBehaviors/IExecNoCommand.cs | 19 - .../ModuleBehaviors/IExecOnMessage.cs | 21 - .../ModuleBehaviors/IExecPostCommand.cs | 22 - .../ModuleBehaviors/IExecPreCommand.cs | 25 - .../ModuleBehaviors/IInputTransformer.cs | 25 - .../ModuleBehaviors/IReadyExecutor.cs | 13 - .../Patronage/FeatureLimitKey.cs | 7 - .../Patronage/FeatureQuotaStats.cs | 8 - src/Ellie.Bot.Common/Patronage/IPatronData.cs | 11 - .../Patronage/IPatronageService.cs | 56 - .../Patronage/ISubscriptionHandler.cs | 16 - src/Ellie.Bot.Common/Patronage/Patron.cs | 38 - .../Patronage/PatronConfigData.cs | 37 - .../Patronage/PatronExtensions.cs | 33 - src/Ellie.Bot.Common/Patronage/PatronTier.cs | 14 - src/Ellie.Bot.Common/Patronage/QuotaLimit.cs | 66 - src/Ellie.Bot.Common/Patronage/QuotaPer.cs | 8 - .../Patronage/SubscriptionChargeStatus.cs | 10 - .../Patronage/UserQuotaStats.cs | 25 - .../Replacements/ReplacementBuilder.cs | 164 -- src/Ellie.Bot.Common/Replacements/Replacer.cs | 93 -- .../Services/CommandHandler.cs | 427 ------ .../Services/Currency/CurrencyService.cs | 109 -- .../Currency/CurrencyServiceExtensions.cs | 39 - .../Services/Currency/DefaultWallet.cs | 115 -- .../Services/Currency/GamblingTxTracker.cs | 110 -- .../Services/IBehaviourExecutor.cs | 17 - .../Services/ICommandHandler.cs | 12 - src/Ellie.Bot.Common/Services/ICoordinator.cs | 20 - .../Services/ICustomBehavior.cs | 13 - src/Ellie.Bot.Common/Services/IEService.cs | 9 - .../Services/IEmbedBuilderService.cs | 81 - .../Services/IGoogleApiService.cs | 19 - .../Services/ILocalDataCache.cs | 13 - .../Services/ILocalization.cs | 19 - .../Services/IRemindService.cs | 15 - .../Services/IStatsService.cs | 51 - .../Services/ITimezoneService.cs | 6 - .../Services/Impl/BehaviorExecutor.cs | 302 ---- .../Services/Impl/BlacklistService.cs | 136 -- .../Services/Impl/CommandsUtilityService.cs | 172 --- .../Impl/DiscordPermOverrideService.cs | 136 -- .../Services/Impl/FontProvider.cs | 60 - .../Services/Impl/IImageCache.cs | 17 - .../Services/Impl/ImagesConfig.cs | 19 - .../Services/Impl/RedisImageExtensions.cs | 11 - .../Services/Impl/SingleProcessCoordinator.cs | 58 - .../Impl/StartingGuildsListService.cs | 18 - .../Services/Impl/StatsService.cs | 188 --- .../Services/Impl/YtdlOperation.cs | 77 - .../Services/strings/impl/BotStrings.cs | 101 -- .../strings/impl/LocalFileStringsSource.cs | 73 - .../strings/impl/MemoryBotStringsProvider.cs | 38 - .../Settings/BotConfigService.cs | 66 - .../Settings/ConfigParsers.cs | 50 - .../Settings/ConfigServiceBase.cs | 201 --- .../Settings/IConfigMigrator.cs | 7 - .../Settings/IConfigService.cs | 46 - .../Settings/SettingParser.cs | 8 - .../SmartText/SmartEmbedText.cs | 184 --- .../SmartText/SmartEmbedTypeArray.cs | 34 - .../SmartText/SmartPlainText.cs | 19 - src/Ellie.Bot.Common/SmartText/SmartText.cs | 89 -- .../SmartText/SmartTextEmbedAuthor.cs | 14 - .../SmartText/SmartTextEmbedField.cs | 9 - .../SmartText/SmartTextEmbedFooter.cs | 12 - src/Ellie.Bot.Common/TypeReaderResult.cs | 30 - .../TypeReaders/CommandOrExprInfo.cs | 23 - .../TypeReaders/GuildDateTimeTypeReader.cs | 49 - src/Ellie.Bot.Common/Yml/CommentAttribute.cs | 11 - .../Yml/CommentGatheringTypeInspector.cs | 65 - .../Yml/CommentsObjectDescriptor.cs | 30 - .../Yml/CommentsObjectGraphVisitor.cs | 29 - .../Yml/MultilineScalarFlowStyleEmitter.cs | 35 - src/Ellie.Bot.Common/Yml/Rgba32Converter.cs | 47 - src/Ellie.Bot.Common/Yml/UrlConverter.cs | 25 - src/Ellie.Bot.Common/Yml/Yaml.cs | 28 - .../_Extensions/BotCredentialsExtensions.cs | 10 - .../_Extensions/Extensions.cs | 207 --- .../_Extensions/IMessageChannelExtensions.cs | 328 ---- .../_Extensions/LinkedListExtensions.cs | 18 - .../_Extensions/NumberExtensions.cs | 7 - .../_Extensions/ReflectionExtensions.cs | 23 - .../_Extensions/Rgba32Extensions.cs | 57 - .../SocketMessageComponentExtensions.cs | 96 -- .../_Extensions/UserExtensions.cs | 39 - src/Ellie.Bot.Common/_common/AddRemove.cs | 10 - src/Ellie.Bot.Common/_common/CmdStrings.cs | 17 - src/Ellie.Bot.Common/_common/CommandData.cs | 9 - .../_common/DownloadTracker.cs | 38 - src/Ellie.Bot.Common/_common/Helpers.cs | 13 - src/Ellie.Bot.Common/_common/ImageUrls.cs | 51 - .../JsonConverters/CultureInfoConverter.cs | 14 - .../_common/JsonConverters/Rgba32Converter.cs | 14 - src/Ellie.Bot.Common/_common/LbOpts.cs | 14 - .../_common/Linq2DbExpressions.cs | 16 - .../_common/LoginErrorHandler.cs | 52 - src/Ellie.Bot.Common/_common/OldCreds.cs | 46 - src/Ellie.Bot.Common/_common/OptionsParser.cs | 23 - src/Ellie.Bot.Common/_common/OsuMapData.cs | 9 - src/Ellie.Bot.Common/_common/OsuUserBets.cs | 58 - .../_common/Pokemon/PokemonNameId.cs | 8 - .../_common/Pokemon/SearchPokemon.cs | 42 - .../_common/Pokemon/SearchPokemonAbility.cs | 10 - ...RequireObjectPropertiesContractResolver.cs | 15 - .../_common/TriviaQuestionModel.cs | 11 - .../_common/TypeReaders/EmoteTypeReader.cs | 13 - .../_common/TypeReaders/GuildTypeReader.cs | 24 - .../TypeReaders/GuildUserTypeReader.cs | 33 - .../_common/TypeReaders/KwumTypeReader.cs | 19 - .../TypeReaders/Models/PermissionAction.cs | 27 - .../_common/TypeReaders/Models/StoopidTime.cs | 55 - .../_common/TypeReaders/ModuleTypeReader.cs | 50 - .../TypeReaders/PermissionActionTypeReader.cs | 39 - .../_common/TypeReaders/Rgba32TypeReader.cs | 20 - .../TypeReaders/StoopidTimeTypeReader.cs | 22 - src/Ellie.Bot.Db/Extensions/ClubExtensions.cs | 34 - .../CurrencyTransactionExtensions.cs | 20 - src/Ellie.Bot.Db/Extensions/DbExtensions.cs | 12 - .../Extensions/DiscordUserExtensions.cs | 126 -- .../Extensions/EllieExpressionExtensions.cs | 15 - .../Extensions/GuildConfigExtensions.cs | 229 --- .../Extensions/QuoteExtensions.cs | 57 - .../Extensions/ReminderExtensions.cs | 23 - .../SelfAssignableRolesExtensions.cs | 22 - .../Extensions/UserXpExtensions.cs | 72 - .../Extensions/WarningExtensions.cs | 60 - src/Ellie.Bot.Db/GlobalUsings.cs | 1 - src/Ellie.Bot.Db/Helpers/ActivityType.cs | 6 - src/Ellie.Bot.Db/Helpers/GuildPerm.cs | 47 - src/Ellie.Bot.Db/LevelStats.cs | 41 - src/Ellie.Bot.Db/Models/AutoCommand.cs | 14 - src/Ellie.Bot.Db/Models/AutoPublishChannel.cs | 9 - .../Models/AutoTranslateChannel.cs | 10 - src/Ellie.Bot.Db/Models/AutoTranslateUser.cs | 11 - src/Ellie.Bot.Db/Models/BlacklistEntry.cs | 15 - src/Ellie.Bot.Db/Models/CommandAlias.cs | 8 - src/Ellie.Bot.Db/Models/CommandCooldown.cs | 8 - .../Models/CurrencyTransaction.cs | 12 - src/Ellie.Bot.Db/Models/DbEntity.cs | 12 - src/Ellie.Bot.Db/Models/DelMsgOnCmdChannel.cs | 14 - .../Models/DiscordPermOverride.cs | 12 - src/Ellie.Bot.Db/Models/DiscordUser.cs | 31 - src/Ellie.Bot.Db/Models/Event.cs | 49 - src/Ellie.Bot.Db/Models/FeedSub.cs | 19 - src/Ellie.Bot.Db/Models/FollowedStream.cs | 34 - src/Ellie.Bot.Db/Models/GCChannelId.cs | 14 - src/Ellie.Bot.Db/Models/GamblingStats.cs | 9 - src/Ellie.Bot.Db/Models/GroupName.cs | 11 - src/Ellie.Bot.Db/Models/GuildConfig.cs | 108 -- src/Ellie.Bot.Db/Models/IgnoredLogItem.cs | 16 - .../Models/IgnoredVoicePresenceChannel.cs | 8 - src/Ellie.Bot.Db/Models/ImageOnlyChannel.cs | 15 - src/Ellie.Bot.Db/Models/LogSetting.cs | 37 - src/Ellie.Bot.Db/Models/NsfwBlacklistedTag.cs | 14 - src/Ellie.Bot.Db/Models/Permission.cs | 55 - src/Ellie.Bot.Db/Models/PlantedCurrency.cs | 12 - src/Ellie.Bot.Db/Models/PlaylistSong.cs | 19 - src/Ellie.Bot.Db/Models/Poll.cs | 17 - src/Ellie.Bot.Db/Models/PollVote.cs | 14 - src/Ellie.Bot.Db/Models/ReactionRole.cs | 18 - src/Ellie.Bot.Db/Models/Reminder.cs | 19 - src/Ellie.Bot.Db/Models/Repeater.cs | 15 - .../Models/RotatingPlayingStatus.cs | 10 - src/Ellie.Bot.Db/Models/SelfAssignableRole.cs | 11 - src/Ellie.Bot.Db/Models/ShopEntry.cs | 43 - .../Models/StreamOnlineMessage.cs | 13 - src/Ellie.Bot.Db/Models/StreamRoleSettings.cs | 68 - src/Ellie.Bot.Db/Models/VcRoleInfo.cs | 8 - .../Models/anti/AntiAltSetting.cs | 11 - .../Models/anti/AntiRaidSetting.cs | 19 - .../Models/anti/AntiSpamIgnore.cs | 12 - .../Models/anti/AntiSpamSetting.cs | 13 - src/Ellie.Bot.Db/Models/club/ClubInfo.cs | 42 - src/Ellie.Bot.Db/Models/currency/BankUser.cs | 9 - .../Models/expr/EllieExpression.cs | 27 - src/Ellie.Bot.Db/Models/expr/Quote.cs | 27 - .../Models/filter/FilterChannelId.cs | 30 - .../Models/filter/FilterLinksChannelId.cs | 13 - .../Models/filter/FilteredWord.cs | 7 - src/Ellie.Bot.Db/Models/punish/BanTemplate.cs | 9 - src/Ellie.Bot.Db/Models/punish/MutedUserId.cs | 13 - .../Models/punish/PunishmentAction.cs | 15 - .../Models/punish/WarnExpireAction.cs | 8 - src/Ellie.Bot.Db/Models/punish/Warning.cs | 13 - .../Models/punish/WarningPunishment.cs | 10 - .../Models/slowmode/SlowmodeIgnoredRole.cs | 20 - .../Models/slowmode/SlowmodeIgnoredUser.cs | 20 - .../Models/support/PatronQuota.cs | 48 - src/Ellie.Bot.Db/Models/support/RewardUser.cs | 10 - src/Ellie.Bot.Db/Models/untimer/UnbanTimer.cs | 14 - .../Models/untimer/UnmuteTimer.cs | 14 - .../Models/untimer/UnroleTimer.cs | 15 - src/Ellie.Bot.Db/Models/xp/UserXpStats.cs | 13 - src/Ellie.Bot.Db/Models/xp/XpSettings.cs | 62 - .../CloneableGenerator.cs | 258 ---- .../SymbolExtensions.cs | 23 - .../SyntaxReceiver.cs | 27 - .../LocalizedStringsGenerator.cs | 140 -- .../Administration.cs | 405 ----- .../AdministrationService.cs | 158 -- .../AutoAssignRoleCommands.cs | 58 - .../AutoAssignRoleService.cs | 159 -- .../AutoPublishService.cs | 87 -- .../DangerousCommands/DangerousCommands.cs | 80 - .../DangerousCommandsService.cs | 104 -- .../GameVoiceChannelCommands.cs | 36 - .../GameVoiceChannelService.cs | 127 -- .../GreetBye/GreetCommands.cs | 229 --- .../GreetBye/GreetGrouper.cs | 71 - .../GreetBye/GreetService.cs | 619 -------- .../GreetBye/GreetSettings.cs | 45 - .../ImageOnlyChannelService.cs | 235 --- .../LocalizationCommands.cs | 250 --- .../Mute/MuteCommands.cs | 231 --- .../Mute/MuteService.cs | 505 ------ .../DiscordPermOverrideCommands.cs | 80 - .../PlayingRotate/PlayingRotateCommands.cs | 60 - .../PlayingRotate/PlayingRotateService.cs | 109 -- .../PrefixCommands.cs | 57 - .../Protection/ProtectionCommands.cs | 288 ---- .../Protection/ProtectionService.cs | 499 ------ .../Protection/ProtectionStats.cs | 52 - .../Protection/PunishQueueItem.cs | 13 - .../Protection/UserSpamStats.cs | 64 - .../Prune/PruneCommands.cs | 114 -- .../Prune/PruneService.cs | 89 -- .../Role/IReactionRoleService.cs | 52 - .../Role/ReactionRoleCommands.cs | 171 --- .../Role/ReactionRolesService.cs | 396 ----- .../Role/RoleCommands.cs | 184 --- .../Self/CheckForUpdatesService.cs | 153 -- .../Self/SelfCommands.cs | 572 ------- .../Self/SelfService.cs | 399 ----- .../SelfAssignedRolesCommands.cs | 234 --- .../SelfAssignedRolesService.cs | 234 --- .../ServerLog/DummyLogCommandService.cs | 25 - .../ServerLog/ServerLogCommandService.cs | 1358 ----------------- .../ServerLog/ServerLogCommands.cs | 171 --- .../Timezone/GuildTimezoneService.cs | 78 - .../Timezone/TimeZoneCommands.cs | 75 - .../UserPunish/UserPunishCommands.cs | 932 ----------- .../UserPunish/UserPunishService.cs | 595 -------- .../VcRole/VcRoleCommands.cs | 77 - .../VcRole/VcRoleService.cs | 209 --- .../EllieExpressions.cs | 395 ----- .../EllieExpressionsExtensios.cs | 88 -- .../EllieExpressionsService.cs | 764 ---------- .../ExportedExpr.cs | 27 - .../ExprField.cs | 10 - .../TypeReaders/CommandOrExprTypeReader.cs | 34 - .../Gambling/AnimalRacing/AnimalRace.cs | 154 -- .../AnimalRacing/AnimalRaceService.cs | 9 - .../AnimalRacing/AnimalRacingCommands.cs | 183 --- .../Gambling/AnimalRacing/AnimalRacingUser.cs | 26 - .../Exceptions/AlreadyJoinedException.cs | 19 - .../Exceptions/AlreadyStartedException.cs | 19 - .../Exceptions/AnimalRaceFullException.cs | 19 - .../Exceptions/NotEnoughFundsException.cs | 19 - .../Gambling/AnimalRacing/RaceOptions.cs | 16 - .../Gambling/Bank/BankCommands.cs | 118 -- .../Gambling/Bank/BankService.cs | 119 -- .../Gambling/BlackJack/BlackJackCommands.cs | 183 --- .../Gambling/BlackJack/BlackJackService.cs | 9 - .../Gambling/BlackJack/Blackjack.cs | 329 ---- .../Gambling/BlackJack/Player.cs | 58 - .../Gambling/CleanupCommands.cs | 0 .../Gambling/Connect4/Connect4.cs | 409 ----- .../Gambling/Connect4/Connect4Commands.cs | 204 --- .../Gambling/DiceRoll/DiceRollCommands.cs | 224 --- .../Gambling/Draw/DrawCommands.cs | 234 --- .../Gambling/EconomyResult.cs | 12 - .../Gambling/Events/CurrencyEventsCommands.cs | 60 - .../Gambling/Events/CurrencyEventsService.cs | 67 - .../Gambling/Events/EventOptions.cs | 39 - .../Gambling/Events/GameStatusEvent.cs | 195 --- .../Gambling/Events/ICurrencyEvent.cs | 9 - .../Gambling/Events/ReactionEvent.cs | 194 --- .../Gambling/FlipCoin/FlipCoinCommands.cs | 140 -- .../Gambling/FlipCoin/FlipResult.cs | 7 - .../Gambling/Gambling.cs | 1014 ------------ .../Gambling/GamblingConfig.cs | 387 ----- .../Gambling/GamblingConfigService.cs | 186 --- .../Gambling/GamblingService.cs | 221 --- .../Gambling/GamblingTopLevelModule.cs | 68 - .../Gambling/GlobalUsings.cs | 31 - .../Gambling/InputRpsPick.cs | 2 - .../PlantPick/PlantAndPickCommands.cs | 112 -- .../Gambling/PlantPick/PlantPickService.cs | 385 ----- .../Gambling/Raffle/CurrencyRaffleCommands.cs | 57 - .../Gambling/Raffle/CurrencyRaffleGame.cs | 69 - .../Gambling/Raffle/CurrencyRaffleService.cs | 81 - .../Gambling/Shop/IShopService.cs | 43 - .../Gambling/Shop/ShopCommands.cs | 496 ------ .../Gambling/Shop/ShopService.cs | 112 -- .../Gambling/Slot/SlotCommands.cs | 227 --- .../Gambling/VoteRewardService.cs | 106 -- .../Gambling/Waifus/WaifuClaimCommands.cs | 373 ----- .../Gambling/Waifus/WaifuService.cs | 583 ------- .../Gambling/Waifus/_common/AffinityTitle.cs | 16 - .../Gambling/Waifus/_common/ClaimTitle.cs | 18 - .../Gambling/Waifus/_common/DivorceResult.cs | 10 - .../Gambling/Waifus/_common/Extensions.cs | 6 - .../Waifus/_common/WaifuClaimResult.cs | 9 - .../Gambling/Waifus/db/Waifu.cs | 19 - .../Gambling/Waifus/db/WaifuExtensions.cs | 133 -- .../Gambling/Waifus/db/WaifuInfoStats.cs | 14 - .../Gambling/Waifus/db/WaifuItem.cs | 10 - .../Gambling/Waifus/db/WaifuLbResult.cs | 16 - .../Gambling/Waifus/db/WaifuUpdate.cs | 17 - .../Gambling/Waifus/db/WaifuUpdateType.cs | 8 - .../Gambling/_common/Decks/QuadDeck.cs | 19 - .../_common/IGamblingCleanupService.cs | 68 - .../Gambling/_common/IGamblingService.cs | 18 - .../Gambling/_common/NewGamblingService.cs | 279 ---- .../Gambling/_common/RollDuelGame.cs | 139 -- .../BaseShmartInputAmountReader.cs | 95 -- .../ShmartBankInputAmountReader.cs | 21 - .../TypeReaders/ShmartNumberTypeReader.cs | 57 - .../Games/Acrophobia/Acrophobia.cs | 200 --- .../Games/Acrophobia/AcrophobiaUser.cs | 22 - .../Games/Acrophobia/AcropobiaCommands.cs | 140 -- .../Games/ChatterBot/ChatterbotService.cs | 217 --- .../Games/ChatterBot/CleverBotCommands.cs | 48 - .../ChatterBot/_common/CleverbotResponse.cs | 8 - .../Games/ChatterBot/_common/Gpt3Response.cs | 30 - .../ChatterBot/_common/IChatterBotSession.cs | 7 - .../_common/OfficialCleverbotSession.cs | 38 - .../ChatterBot/_common/OfficialGpt3Session.cs | 68 - src/Ellie.Bot.Modules.Gambling/Games/Games.cs | 150 -- .../Games/GamesConfig.cs | 160 -- .../Games/GamesConfigService.cs | 72 - .../Games/GamesService.cs | 118 -- .../Games/GirlRating.cs | 61 - .../Games/Hangman/DefaultHangmanSource.cs | 64 - .../Games/Hangman/HangmanCommands.cs | 76 - .../Games/Hangman/HangmanGame.cs | 112 -- .../Games/Hangman/HangmanService.cs | 136 -- .../Games/Hangman/HangmanTerm.cs | 8 - .../Games/Hangman/IHangmanService.cs | 10 - .../Games/Hangman/IHangmanSource.cs | 10 - .../Games/Nunchi/Nunchi.cs | 183 --- .../Games/Nunchi/NunchiCommands.cs | 111 -- .../Games/Polls/PollCommands.cs | 102 -- .../Games/Polls/PollExtensions.cs | 36 - .../Games/Polls/PollRunner.cs | 63 - .../Games/Polls/PollService.cs | 140 -- .../Games/SpeedTyping/SpeedTypingCommands.cs | 103 -- .../Games/SpeedTyping/TypingArticle.cs | 9 - .../Games/SpeedTyping/TypingGame.cs | 183 --- .../Games/TicTacToe/TicTacToe.cs | 307 ---- .../Games/TicTacToe/TicTacToeCommands.cs | 54 - .../Games/Trivia/Games.cs | 275 ---- .../QuestionPool/DefaultQuestionPool.cs | 22 - .../Trivia/QuestionPool/IQuestionPool.cs | 6 - .../QuestionPool/PokemonQuestionPool.cs | 32 - .../Games/Trivia/TriviaGame.cs | 219 --- .../Games/Trivia/TriviaGamesService.cs | 37 - .../Games/Trivia/TriviaOptions.cs | 44 - .../Games/Trivia/TriviaQuestion.cs | 115 -- .../Games/Trivia/TriviaUser.cs | 3 - .../CommandJsonObject.cs | 13 - src/Ellie.Bot.Modules.Help/CommandsOptions.cs | 26 - src/Ellie.Bot.Modules.Help/Help.cs | 588 ------- src/Ellie.Bot.Modules.Help/HelpService.cs | 42 - .../CleanupCommands.cs | 24 - src/Ellie.Bot.Modules.Music/Music.cs | 758 --------- .../PlaylistCommands.cs | 229 --- .../Services/AyuVoiceStateService.cs | 218 --- .../Services/IMusicService.cs | 36 - .../Services/MusicService.cs | 460 ------ .../Services/SoundCloudApiService.cs | 78 - .../Services/extractor/Misc.cs | 71 - .../Services/extractor/YtLoader.cs | 130 -- .../_common/ICachableTrackData.cs | 12 - .../_common/ILocalTrackResolver.cs | 7 - .../_common/IMusicPlayer.cs | 40 - .../_common/IMusicQueue.cs | 22 - .../_common/IPlatformQueryResolver.cs | 6 - .../_common/IQueuedTrackInfo.cs | 9 - .../_common/IRadioResolver.cs | 6 - .../_common/ISoundcloudResolver.cs | 8 - .../_common/ITrackCacher.cs | 25 - .../_common/ITrackInfo.cs | 12 - .../_common/ITrackResolveProvider.cs | 6 - .../_common/IVoiceProxy.cs | 15 - .../_common/IYoutubeResolver.cs | 11 - .../_common/Impl/CachableTrackData.cs | 19 - .../_common/Impl/MultimediaTimer.cs | 95 -- .../_common/Impl/MusicExtensions.cs | 57 - .../_common/Impl/MusicPlatform.cs | 10 - .../_common/Impl/MusicPlayer.cs | 528 ------- .../_common/Impl/MusicQueue.cs | 316 ---- .../_common/Impl/RemoteTrackInfo.cs | 16 - .../_common/Impl/SimpleTrackInfo.cs | 30 - .../_common/Impl/TrackCacher.cs | 105 -- .../_common/Impl/VoiceProxy.cs | 102 -- .../_common/Resolvers/LocalTrackResolver.cs | 122 -- .../_common/Resolvers/RadioResolveStrategy.cs | 106 -- .../_common/Resolvers/SoundcloudResolver.cs | 84 - .../_common/Resolvers/TrackResolveProvider.cs | 56 - .../_common/Resolvers/YtdlYoutubeResolver.cs | 315 ---- .../db/MusicPlayerSettingsExtensions.cs | 27 - .../_common/db/MusicPlaylist.cs | 10 - .../_common/db/MusicPlaylistExtensions.cs | 19 - .../_common/db/MusicSettings.cs | 61 - .../Patronage/Config/PatronageConfig.cs | 36 - .../Patronage/CurrencyRewardService.cs | 195 --- .../Patronage/InsufficientTier.cs | 11 - .../Patronage/Patreon/PatreonClient.cs | 149 -- .../Patronage/Patreon/PatreonCredentials.cs | 10 - .../Patronage/Patreon/PatreonData.cs | 134 -- .../Patronage/Patreon/PatreonMemberData.cs | 33 - .../Patronage/Patreon/PatreonRefreshData.cs | 22 - .../Patreon/PatreonSubscriptionHandler.cs | 79 - .../Patronage/PatronageCommands.cs | 148 -- .../Patronage/PatronageService.cs | 837 ---------- .../Blacklist/BlacklistCommands.cs | 143 -- .../CleverbotResponseCmdCdTypeReader.cs | 15 - .../CommandCooldown/CmdCdService.cs | 143 -- .../CommandCooldown/CmdCdsCommands.cs | 105 -- .../Filter/FilterCommands.cs | 324 ---- .../Filter/FilterService.cs | 241 --- .../Filter/ServerFilterSettings.cs | 10 - .../GlobalPermissionCommands.cs | 77 - .../GlobalPermissionService.cs | 92 -- .../PermissionCache.cs | 11 - .../PermissionExtensions.cs | 132 -- .../Permissions.cs | 516 ------- .../PermissionsCollection.cs | 74 - .../PermissionsService.cs | 184 --- .../ResetPermissionsCommands.cs | 37 - .../Anime/AnimeResult.cs | 41 - .../Anime/AnimeSearchCommands.cs | 204 --- .../Anime/AnimeSearchService.cs | 79 - .../Anime/MangaResult.cs | 40 - .../Crypto/CryptoCommands.cs | 196 --- .../Crypto/CryptoService.cs | 216 --- .../Crypto/DefaultStockDataService.cs | 103 -- .../Crypto/Drawing/CandleDrawingData.cs | 12 - .../Drawing/IStockChartDrawingService.cs | 8 - .../ImagesharpStockChartDrawingService.cs | 202 --- .../Crypto/IStockDataService.cs | 8 - .../Crypto/Polygon/FinnHubSearchResponse.cs | 13 - .../Crypto/Polygon/FinnHubSearchResult.cs | 19 - .../Crypto/Polygon/PolygonApiClient.cs | 55 - .../Crypto/Polygon/PolygonStockDataService.cs | 26 - .../Crypto/Polygon/PolygonTickerData.cs | 43 - .../Crypto/Polygon/PolygonTickerResponse.cs | 13 - .../Crypto/_common/CandleData.cs | 9 - .../Crypto/_common/ImageData.cs | 7 - .../Crypto/_common/QuoteResponse.cs | 43 - .../Crypto/_common/StockData.cs | 15 - .../Crypto/_common/SymbolData.cs | 3 - .../Crypto/_common/YahooFinanceCandleData.cs | 12 - .../_common/YahooFinanceSearchResponse.cs | 19 - .../_common/YahooFinanceSearchResponseItem.cs | 25 - .../Crypto/_common/YahooQueryModel.cs | 9 - .../Feeds/FeedCommands.cs | 115 -- .../Feeds/FeedsService.cs | 280 ---- .../JokeCommands.cs | 53 - .../MemegenCommands.cs | 96 -- src/Ellie.Bot.Modules.Searches/OsuCommands.cs | 297 ---- .../PathOfExileCommands.cs | 311 ---- .../PlaceCommands.cs | 71 - .../PokemonSearchCommands.cs | 74 - src/Ellie.Bot.Modules.Searches/Searches.cs | 612 -------- .../SearchesService.cs | 468 ------ .../StreamNotificationCommands.cs | 200 --- .../StreamNotificationService.cs | 617 -------- .../StreamOnlineMessageDeleterService.cs | 99 -- .../Translate/ITranslateService.cs | 17 - .../Translate/TranslateService.cs | 224 --- .../Translate/TranslatorCommands.cs | 96 -- .../XkcdCommands.cs | 97 -- .../YoutubeTrack/YtTrackService.cs | 134 -- .../YoutubeTrack/YtUploadCommands.cs | 54 - .../_common/AltExtensions.cs | 12 - .../_common/BibleVerses.cs | 21 - .../_common/Config/ImgSearchEngine.cs | 7 - .../_common/Config/SearchesConfig.cs | 77 - .../_common/Config/SearchesConfigService.cs | 45 - .../_common/Config/WebSearchEngine.cs | 9 - .../_common/CryptoData.cs | 66 - .../_common/DefineModel.cs | 43 - .../_common/E621Object.cs | 24 - .../Exceptions/StreamNotFoundException.cs | 18 - .../_common/Extensions.cs | 9 - .../_common/Gallery.cs | 44 - .../_common/GatariUserResponse.cs | 52 - .../_common/GatariUserStatsResponse.cs | 76 - .../_common/GoogleSearchResult.cs | 16 - .../_common/HearthstoneCardData.cs | 13 - .../_common/LowerCaseNamingPolicy.cs | 12 - .../_common/MagicItem.cs | 8 - .../_common/MtgData.cs | 26 - .../_common/NovelData.cs | 14 - .../_common/OmdbMovie.cs | 13 - .../_common/OsuUserData.cs | 70 - .../_common/PathOfExileModels.cs | 40 - .../_common/SteamGameId.cs | 35 - .../Models/HelixStreamsResponse.cs | 64 - .../Models/HelixUsersResponse.cs | 46 - .../Models/PicartoChannelResponse.cs | 157 -- .../StreamNotifications/Models/StreamData.cs | 21 - .../Models/StreamDataKey.cs | 16 - .../Models/TrovoGetUsersResponse.cs | 61 - .../Models/TrovoRequestData.cs | 10 - .../Models/TrovoSocialLink.cs | 13 - .../Models/TwitchResponseV5.cs | 114 -- .../Models/TwitchUsersResponseV5.cs | 37 - .../StreamNotifications/NotifChecker.cs | 215 --- .../Providers/PicartoProvider.cs | 103 -- .../StreamNotifications/Providers/Provider.cs | 63 - .../Providers/TrovoProvider.cs | 126 -- .../Providers/TwitchHelixProvider.cs | 197 --- .../_common/TimeData.cs | 9 - .../_common/TimeModels.cs | 22 - .../_common/UrbanDef.cs | 14 - .../_common/WeatherModels.cs | 67 - .../_common/WikipediaApiModel.cs | 18 - .../_common/WoWJoke.cs | 11 - .../Alias/AliasCommands.cs | 137 -- .../Alias/AliasService.cs | 95 -- .../Calc/CalcCommands.cs | 48 - .../ConfigCommands.cs | 154 -- .../Info/InfoCommands.cs | 215 --- .../Info/InviteCommands.cs | 93 -- .../Invite/InviteService.cs | 48 - .../Quote/IQuoteService.cs | 6 - .../Quote/QuoteCommands.cs | 416 ----- .../Quote/QuoteService.cs | 32 - .../Remind/RemindCommands.cs | 225 --- .../Remind/RemindService.cs | 254 --- .../Repeater/RepeatCommands.cs | 204 --- .../Repeater/RepeaterService.cs | 422 ----- .../Repeater/RunningRepeater.cs | 92 -- .../StreamRole/StreamRoleCommands.cs | 97 -- .../StreamRole/StreamRoleService.cs | 339 ---- .../UnitConversion/ConverterService.cs | 99 -- .../UnitConversion/UnitConversionCommands.cs | 96 -- src/Ellie.Bot.Modules.Utility/Utility.cs | 640 -------- .../VerboseErrors/EvalCommands.cs | 76 - .../VerboseErrors/EvalGlobals.cs | 13 - .../VerboseErrorsService.cs | 69 - .../_common/ConvertUnit.cs | 12 - .../Exceptions/StreamRoleNotFoundException.cs | 20 - .../StreamRolePermissionException.cs | 20 - .../_common/StreamRoleListType.cs | 8 - src/Ellie.Bot.Modules.Xp/CleanupCommands.cs | 19 - src/Ellie.Bot.Modules.Xp/Club/Club.cs | 424 ----- src/Ellie.Bot.Modules.Xp/Club/ClubService.cs | 319 ---- src/Ellie.Bot.Modules.Xp/Club/IClubService.cs | 33 - .../Club/Results/ClubAcceptResult.cs | 8 - .../Club/Results/ClubBanResult.cs | 9 - .../Club/Results/ClubCreateResult.cs | 9 - .../Club/Results/ClubKickResult.cs | 9 - .../Club/Results/ClubLeaveResult.cs | 8 - .../Club/Results/ClubTransferError.cs | 7 - .../Club/Results/ClubUnbanResult.cs | 8 - .../Club/Results/SetClubIconResult.cs | 9 - .../Club/Results/ToggleAdminResult.cs | 10 - src/Ellie.Bot.Modules.Xp/Xp.cs | 559 ------- src/Ellie.Bot.Modules.Xp/XpConfig.cs | 109 -- src/Ellie.Bot.Modules.Xp/XpConfigService.cs | 63 - src/Ellie.Bot.Modules.Xp/XpRewards.cs | 138 -- src/Ellie.Bot.Modules.Xp/XpService.cs | 1252 --------------- .../_common/Extensions.cs | 7 - .../_common/FullUserStats.cs | 32 - .../_common/IXpCleanupService.cs | 6 - .../_common/UserCacheItem.cs | 13 - .../_common/XpCleanupService.cs | 32 - .../_common/XpTemplate.cs | 271 ---- .../_common/db/XpShopOwnedItem.cs | 18 - src/Ellie.Coordinator/CoordStartup.cs | 48 - src/Ellie.Coordinator/LogSetup.cs | 43 - src/Ellie.Coordinator/Program.cs | 20 - .../Properties/launchSettings.json | 13 - .../Protos/coordinator.proto | 127 -- .../Services/CoordinatorRunner.cs | 457 ------ .../Services/CoordinatorService.cs | 144 -- src/Ellie.Coordinator/Shared/Config.cs | 21 - src/Ellie.Coordinator/Shared/CoordState.cs | 9 - .../Shared/JsonStatusObject.cs | 9 - src/Ellie.Coordinator/Shared/ShardStatus.cs | 15 - .../appsettings.Development.json | 9 - src/Ellie.Coordinator/appsettings.json | 20 - src/Ellie.Coordinator/coord.yml | 12 - src/Ellie.Econ/Deck/Deck.cs | 309 ---- src/Ellie.Econ/Deck/NewCard.cs | 5 - src/Ellie.Econ/Deck/NewDeck.cs | 54 - .../MultipleRegularDeck.cs | 28 - src/Ellie.Econ/Deck/Regular/RegularCard.cs | 4 - src/Ellie.Econ/Deck/Regular/RegularDeck.cs | 15 - .../Deck/Regular/RegularDeckExtensions.cs | 43 - src/Ellie.Econ/Deck/Regular/RegularSuit.cs | 9 - src/Ellie.Econ/Deck/Regular/RegularValue.cs | 18 - .../Gambling/Betdraw/BetdrawColorGuess.cs | 7 - .../Gambling/Betdraw/BetdrawGame.cs | 84 - .../Gambling/Betdraw/BetdrawResult.cs | 9 - .../Gambling/Betdraw/BetdrawResultType.cs | 7 - .../Gambling/Betdraw/BetdrawValueGuess.cs | 7 - .../Gambling/Betflip/BetflipGame.cs | 33 - .../Gambling/Betflip/BetflipResult.cs | 8 - .../Gambling/Betroll/BetrollGame.cs | 42 - .../Gambling/Betroll/BetrollResult.cs | 9 - src/Ellie.Econ/Gambling/Rps/RpsGame.cs | 75 - src/Ellie.Econ/Gambling/Slot/SlotGame.cs | 113 -- src/Ellie.Econ/Gambling/Slot/SlotResult.cs | 9 - src/Ellie.Econ/Gambling/Wof/LulaResult.cs | 9 - src/Ellie.Econ/Gambling/Wof/WofGame.cs | 34 - src/Ellie.Tests/Ellie.Tests.csproj | 21 + src/Ellie.Tests/README.md | 1 + src/Ellie.VotesApi/.dockerignore | 25 - src/Ellie.VotesApi/.gitignore | 1 - src/Ellie.VotesApi/Common/AuthHandler.cs | 41 - src/Ellie.VotesApi/Common/ConfKeys.cs | 8 - .../Common/DiscordsVoteWebhookModel.cs | 26 - src/Ellie.VotesApi/Common/Policies.cs | 8 - .../Common/ToppVoteWebhookModel.cs | 30 - .../Controllers/DiscordsController.cs | 33 - .../Controllers/TopGgController.cs | 34 - .../Controllers/WebhookController.cs | 48 - src/Ellie.VotesApi/Dockerfile | 20 - src/Ellie.VotesApi/Program.cs | 9 - .../Properties/launchSettings.json | 31 - src/Ellie.VotesApi/Services/FileVotesCache.cs | 100 -- src/Ellie.VotesApi/Services/IVotesCache.cs | 13 - src/Ellie.VotesApi/Startup.cs | 68 - src/Ellie.VotesApi/WeatherForecast.cs | 7 - .../appsettings.Development.json | 10 - src/Ellie.VotesApi/appsettings.json | 12 - src/Ellie/Bot.cs | 412 ----- src/Ellie/Db/EllieContext.cs | 488 ------ src/Ellie/Db/EllieDbService.cs | 76 - src/Ellie/Db/MysqlContext.cs | 38 - src/Ellie/Db/PostgreSqlContext.cs | 26 - src/Ellie/Db/SqliteContext.cs | 26 - src/Ellie/Ellie.csproj | 2 +- .../Marmalade/Adapters/BehaviorAdapter.cs | 78 - .../Adapters/ContextAdapterFactory.cs | 7 - .../Marmalade/Adapters/DmContextAdapter.cs | 51 - src/Ellie/Marmalade/Adapters/FilterAdapter.cs | 31 - .../Marmalade/Adapters/GuildContextAdapter.cs | 53 - .../Marmalade/Adapters/ParamParserAdapter.cs | 32 - src/Ellie/Marmalade/CommandContextType.cs | 27 - .../Config/IMarmaladeConfigService.cs | 8 - src/Ellie/Marmalade/Config/MarmaladeConfig.cs | 20 - .../Config/MarmaladeConfigService.cs | 45 - .../Marmalade/MarmaladeAssemblyLoadContext.cs | 35 - .../Marmalade/MarmaladeIoCKernelModule.cs | 98 -- src/Ellie/Marmalade/MarmaladeLoaderService.cs | 917 ----------- .../Marmalade/Models/CanaryCommandData.cs | 44 - src/Ellie/Marmalade/Models/CanaryData.cs | 11 - src/Ellie/Marmalade/Models/ParamData.cs | 10 - .../Marmalade/Models/ResolvedMarmalade.cs | 16 - src/Ellie/Marmalade/RemovablePlanner.cs | 122 -- src/Ellie/Migrations/MigrationQueries.cs | 48 - .../Modules/Gambling/CurrencyProvider.cs | 18 - .../IMarmaladesRepositoryService.cs | 6 - src/Ellie/Modules/Marmalades/Marmalade.cs | 229 --- src/Ellie/Modules/Marmalades/MarmaladeItem.cs | 8 - .../Marmalades/MarmaladesRepositoryService.cs | 22 - src/Ellie/PermissionChecker.cs | 80 - src/Ellie/Program.cs | 28 - src/Ellie/Services/Impl/BotCredsProvider.cs | 204 --- src/Ellie/Services/Impl/GoogleApiService.cs | 227 --- .../GoogleApiService_SupportedLanguages.cs | 160 -- src/Ellie/Services/Impl/ImageCache.cs | 83 - src/Ellie/Services/Impl/LocalDataCache.cs | 108 -- src/Ellie/Services/Impl/Localization.cs | 121 -- src/Ellie/Services/Impl/PubSub/JsonSeria.cs | 27 - src/Ellie/Services/Impl/PubSub/RedisPubSub.cs | 52 - src/Ellie/Services/Impl/PubSub/YamlSeria.cs | 39 - src/Ellie/Services/Impl/RedisBotCache.cs | 119 -- .../Services/Impl/RedisBotStringsProvider.cs | 78 - .../Services/Impl/RemoteGrpcCoordinator.cs | 132 -- src/Ellise.Common/AsyncLazy.cs | 19 - src/Ellise.Common/Cache/BotCacheExtensions.cs | 46 - src/Ellise.Common/Cache/IBotCache.cs | 47 - src/Ellise.Common/Cache/MemoryBotCache.cs | 71 - .../Collections/ConcurrentHashSet.cs | 88 -- .../Collections/IndexedCollection.cs | 148 -- src/Ellise.Common/EllieRandom.cs | 69 - .../Extensions/ArrayExtensions.cs | 51 - .../Extensions/EnumerableExtensions.cs | 109 -- src/Ellise.Common/Extensions/Extensions.cs | 7 - .../Extensions/HttpClientExtensions.cs | 35 - .../Extensions/OneOfExtensions.cs | 10 - .../Extensions/PipeExtensions.cs | 22 - .../Extensions/StringExtensions.cs | 139 -- src/Ellise.Common/Helpers/LogSetup.cs | 36 - .../Helpers/StandardConversions.cs | 7 - src/Ellise.Common/Kwum.cs | 100 -- src/Ellise.Common/PubSub/EventPubSub.cs | 80 - src/Ellise.Common/PubSub/IPubSub.cs | 10 - src/Ellise.Common/PubSub/ISeria.cs | 7 - src/Ellise.Common/QueueRunner.cs | 63 - src/Ellise.Common/TypedKey.cs | 30 - src/Ellise.Common/YamlHelper.cs | 48 - 756 files changed, 26 insertions(+), 66237 deletions(-) delete mode 100644 src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/creds/IBotCredsProvider.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/IBotStringsExtensions.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/IBotStringsProvider.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs delete mode 100644 src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/CmdAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs delete mode 100644 src/Ellie.Bot.Common/BotCommandTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/CleanupModuleBase.cs delete mode 100644 src/Ellie.Bot.Common/CleaverBotResponseStr.cs delete mode 100644 src/Ellie.Bot.Common/CommandNameLoadHelper.cs delete mode 100644 src/Ellie.Bot.Common/Configs/BotConfig.cs delete mode 100644 src/Ellie.Bot.Common/Configs/IConfigSeria.cs delete mode 100644 src/Ellie.Bot.Common/Creds.cs delete mode 100644 src/Ellie.Bot.Common/Currency/CurrencyType.cs delete mode 100644 src/Ellie.Bot.Common/Currency/IBankService.cs delete mode 100644 src/Ellie.Bot.Common/Currency/ICurrencyService.cs delete mode 100644 src/Ellie.Bot.Common/Currency/ITxTracker.cs delete mode 100644 src/Ellie.Bot.Common/Currency/IWallet.cs delete mode 100644 src/Ellie.Bot.Common/Currency/TxData.cs delete mode 100644 src/Ellie.Bot.Common/DbService.cs delete mode 100644 src/Ellie.Bot.Common/DoAsUserMessage.cs delete mode 100644 src/Ellie.Bot.Common/EllieModule.cs delete mode 100644 src/Ellie.Bot.Common/EllieTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/Extensions/DbExtensions.cs delete mode 100644 src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs delete mode 100644 src/Ellie.Bot.Common/GlobalUsings.cs delete mode 100644 src/Ellie.Bot.Common/IBot.cs delete mode 100644 src/Ellie.Bot.Common/ICloneable.cs delete mode 100644 src/Ellie.Bot.Common/ICurrencyProvider.cs delete mode 100644 src/Ellie.Bot.Common/IDiscordPermOverrideService.cs delete mode 100644 src/Ellie.Bot.Common/IEllieCommandOptions.cs delete mode 100644 src/Ellie.Bot.Common/ILogCommandService.cs delete mode 100644 src/Ellie.Bot.Common/IPermissionChecker.cs delete mode 100644 src/Ellie.Bot.Common/IPlaceholderProvider.cs delete mode 100644 src/Ellie.Bot.Common/Interaction/EllieInteraction.cs delete mode 100644 src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs delete mode 100644 src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs delete mode 100644 src/Ellie.Bot.Common/Interaction/IEllieInteractionService.cs delete mode 100644 src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs delete mode 100644 src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs delete mode 100644 src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs delete mode 100644 src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs delete mode 100644 src/Ellie.Bot.Common/MessageType.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs delete mode 100644 src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/IPatronData.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/IPatronageService.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/Patron.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/PatronConfigData.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/PatronExtensions.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/PatronTier.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/QuotaLimit.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/QuotaPer.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/SubscriptionChargeStatus.cs delete mode 100644 src/Ellie.Bot.Common/Patronage/UserQuotaStats.cs delete mode 100644 src/Ellie.Bot.Common/Replacements/ReplacementBuilder.cs delete mode 100644 src/Ellie.Bot.Common/Replacements/Replacer.cs delete mode 100644 src/Ellie.Bot.Common/Services/CommandHandler.cs delete mode 100644 src/Ellie.Bot.Common/Services/Currency/CurrencyService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Currency/CurrencyServiceExtensions.cs delete mode 100644 src/Ellie.Bot.Common/Services/Currency/DefaultWallet.cs delete mode 100644 src/Ellie.Bot.Common/Services/Currency/GamblingTxTracker.cs delete mode 100644 src/Ellie.Bot.Common/Services/IBehaviourExecutor.cs delete mode 100644 src/Ellie.Bot.Common/Services/ICommandHandler.cs delete mode 100644 src/Ellie.Bot.Common/Services/ICoordinator.cs delete mode 100644 src/Ellie.Bot.Common/Services/ICustomBehavior.cs delete mode 100644 src/Ellie.Bot.Common/Services/IEService.cs delete mode 100644 src/Ellie.Bot.Common/Services/IEmbedBuilderService.cs delete mode 100644 src/Ellie.Bot.Common/Services/IGoogleApiService.cs delete mode 100644 src/Ellie.Bot.Common/Services/ILocalDataCache.cs delete mode 100644 src/Ellie.Bot.Common/Services/ILocalization.cs delete mode 100644 src/Ellie.Bot.Common/Services/IRemindService.cs delete mode 100644 src/Ellie.Bot.Common/Services/IStatsService.cs delete mode 100644 src/Ellie.Bot.Common/Services/ITimezoneService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/BehaviorExecutor.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/BlacklistService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/CommandsUtilityService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/DiscordPermOverrideService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/FontProvider.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/IImageCache.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/ImagesConfig.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/RedisImageExtensions.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/SingleProcessCoordinator.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/StartingGuildsListService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/StatsService.cs delete mode 100644 src/Ellie.Bot.Common/Services/Impl/YtdlOperation.cs delete mode 100644 src/Ellie.Bot.Common/Services/strings/impl/BotStrings.cs delete mode 100644 src/Ellie.Bot.Common/Services/strings/impl/LocalFileStringsSource.cs delete mode 100644 src/Ellie.Bot.Common/Services/strings/impl/MemoryBotStringsProvider.cs delete mode 100644 src/Ellie.Bot.Common/Settings/BotConfigService.cs delete mode 100644 src/Ellie.Bot.Common/Settings/ConfigParsers.cs delete mode 100644 src/Ellie.Bot.Common/Settings/ConfigServiceBase.cs delete mode 100644 src/Ellie.Bot.Common/Settings/IConfigMigrator.cs delete mode 100644 src/Ellie.Bot.Common/Settings/IConfigService.cs delete mode 100644 src/Ellie.Bot.Common/Settings/SettingParser.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartEmbedText.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartEmbedTypeArray.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartPlainText.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartText.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartTextEmbedAuthor.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartTextEmbedField.cs delete mode 100644 src/Ellie.Bot.Common/SmartText/SmartTextEmbedFooter.cs delete mode 100644 src/Ellie.Bot.Common/TypeReaderResult.cs delete mode 100644 src/Ellie.Bot.Common/TypeReaders/CommandOrExprInfo.cs delete mode 100644 src/Ellie.Bot.Common/TypeReaders/GuildDateTimeTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/Yml/CommentAttribute.cs delete mode 100644 src/Ellie.Bot.Common/Yml/CommentGatheringTypeInspector.cs delete mode 100644 src/Ellie.Bot.Common/Yml/CommentsObjectDescriptor.cs delete mode 100644 src/Ellie.Bot.Common/Yml/CommentsObjectGraphVisitor.cs delete mode 100644 src/Ellie.Bot.Common/Yml/MultilineScalarFlowStyleEmitter.cs delete mode 100644 src/Ellie.Bot.Common/Yml/Rgba32Converter.cs delete mode 100644 src/Ellie.Bot.Common/Yml/UrlConverter.cs delete mode 100644 src/Ellie.Bot.Common/Yml/Yaml.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/BotCredentialsExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/Extensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/IMessageChannelExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/LinkedListExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/NumberExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/ReflectionExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/Rgba32Extensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/SocketMessageComponentExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_Extensions/UserExtensions.cs delete mode 100644 src/Ellie.Bot.Common/_common/AddRemove.cs delete mode 100644 src/Ellie.Bot.Common/_common/CmdStrings.cs delete mode 100644 src/Ellie.Bot.Common/_common/CommandData.cs delete mode 100644 src/Ellie.Bot.Common/_common/DownloadTracker.cs delete mode 100644 src/Ellie.Bot.Common/_common/Helpers.cs delete mode 100644 src/Ellie.Bot.Common/_common/ImageUrls.cs delete mode 100644 src/Ellie.Bot.Common/_common/JsonConverters/CultureInfoConverter.cs delete mode 100644 src/Ellie.Bot.Common/_common/JsonConverters/Rgba32Converter.cs delete mode 100644 src/Ellie.Bot.Common/_common/LbOpts.cs delete mode 100644 src/Ellie.Bot.Common/_common/Linq2DbExpressions.cs delete mode 100644 src/Ellie.Bot.Common/_common/LoginErrorHandler.cs delete mode 100644 src/Ellie.Bot.Common/_common/OldCreds.cs delete mode 100644 src/Ellie.Bot.Common/_common/OptionsParser.cs delete mode 100644 src/Ellie.Bot.Common/_common/OsuMapData.cs delete mode 100644 src/Ellie.Bot.Common/_common/OsuUserBets.cs delete mode 100644 src/Ellie.Bot.Common/_common/Pokemon/PokemonNameId.cs delete mode 100644 src/Ellie.Bot.Common/_common/Pokemon/SearchPokemon.cs delete mode 100644 src/Ellie.Bot.Common/_common/Pokemon/SearchPokemonAbility.cs delete mode 100644 src/Ellie.Bot.Common/_common/RequireObjectPropertiesContractResolver.cs delete mode 100644 src/Ellie.Bot.Common/_common/TriviaQuestionModel.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/EmoteTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/GuildTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/GuildUserTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/KwumTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/Models/PermissionAction.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/Models/StoopidTime.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/ModuleTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/PermissionActionTypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/Rgba32TypeReader.cs delete mode 100644 src/Ellie.Bot.Common/_common/TypeReaders/StoopidTimeTypeReader.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/ClubExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/CurrencyTransactionExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/DbExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/DiscordUserExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/EllieExpressionExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/GuildConfigExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/QuoteExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/ReminderExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/SelfAssignableRolesExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/UserXpExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Extensions/WarningExtensions.cs delete mode 100644 src/Ellie.Bot.Db/Helpers/ActivityType.cs delete mode 100644 src/Ellie.Bot.Db/Helpers/GuildPerm.cs delete mode 100644 src/Ellie.Bot.Db/LevelStats.cs delete mode 100644 src/Ellie.Bot.Db/Models/AutoCommand.cs delete mode 100644 src/Ellie.Bot.Db/Models/AutoPublishChannel.cs delete mode 100644 src/Ellie.Bot.Db/Models/AutoTranslateChannel.cs delete mode 100644 src/Ellie.Bot.Db/Models/AutoTranslateUser.cs delete mode 100644 src/Ellie.Bot.Db/Models/BlacklistEntry.cs delete mode 100644 src/Ellie.Bot.Db/Models/CommandAlias.cs delete mode 100644 src/Ellie.Bot.Db/Models/CommandCooldown.cs delete mode 100644 src/Ellie.Bot.Db/Models/CurrencyTransaction.cs delete mode 100644 src/Ellie.Bot.Db/Models/DbEntity.cs delete mode 100644 src/Ellie.Bot.Db/Models/DelMsgOnCmdChannel.cs delete mode 100644 src/Ellie.Bot.Db/Models/DiscordPermOverride.cs delete mode 100644 src/Ellie.Bot.Db/Models/DiscordUser.cs delete mode 100644 src/Ellie.Bot.Db/Models/Event.cs delete mode 100644 src/Ellie.Bot.Db/Models/FeedSub.cs delete mode 100644 src/Ellie.Bot.Db/Models/FollowedStream.cs delete mode 100644 src/Ellie.Bot.Db/Models/GCChannelId.cs delete mode 100644 src/Ellie.Bot.Db/Models/GamblingStats.cs delete mode 100644 src/Ellie.Bot.Db/Models/GroupName.cs delete mode 100644 src/Ellie.Bot.Db/Models/GuildConfig.cs delete mode 100644 src/Ellie.Bot.Db/Models/IgnoredLogItem.cs delete mode 100644 src/Ellie.Bot.Db/Models/IgnoredVoicePresenceChannel.cs delete mode 100644 src/Ellie.Bot.Db/Models/ImageOnlyChannel.cs delete mode 100644 src/Ellie.Bot.Db/Models/LogSetting.cs delete mode 100644 src/Ellie.Bot.Db/Models/NsfwBlacklistedTag.cs delete mode 100644 src/Ellie.Bot.Db/Models/Permission.cs delete mode 100644 src/Ellie.Bot.Db/Models/PlantedCurrency.cs delete mode 100644 src/Ellie.Bot.Db/Models/PlaylistSong.cs delete mode 100644 src/Ellie.Bot.Db/Models/Poll.cs delete mode 100644 src/Ellie.Bot.Db/Models/PollVote.cs delete mode 100644 src/Ellie.Bot.Db/Models/ReactionRole.cs delete mode 100644 src/Ellie.Bot.Db/Models/Reminder.cs delete mode 100644 src/Ellie.Bot.Db/Models/Repeater.cs delete mode 100644 src/Ellie.Bot.Db/Models/RotatingPlayingStatus.cs delete mode 100644 src/Ellie.Bot.Db/Models/SelfAssignableRole.cs delete mode 100644 src/Ellie.Bot.Db/Models/ShopEntry.cs delete mode 100644 src/Ellie.Bot.Db/Models/StreamOnlineMessage.cs delete mode 100644 src/Ellie.Bot.Db/Models/StreamRoleSettings.cs delete mode 100644 src/Ellie.Bot.Db/Models/VcRoleInfo.cs delete mode 100644 src/Ellie.Bot.Db/Models/anti/AntiAltSetting.cs delete mode 100644 src/Ellie.Bot.Db/Models/anti/AntiRaidSetting.cs delete mode 100644 src/Ellie.Bot.Db/Models/anti/AntiSpamIgnore.cs delete mode 100644 src/Ellie.Bot.Db/Models/anti/AntiSpamSetting.cs delete mode 100644 src/Ellie.Bot.Db/Models/club/ClubInfo.cs delete mode 100644 src/Ellie.Bot.Db/Models/currency/BankUser.cs delete mode 100644 src/Ellie.Bot.Db/Models/expr/EllieExpression.cs delete mode 100644 src/Ellie.Bot.Db/Models/expr/Quote.cs delete mode 100644 src/Ellie.Bot.Db/Models/filter/FilterChannelId.cs delete mode 100644 src/Ellie.Bot.Db/Models/filter/FilterLinksChannelId.cs delete mode 100644 src/Ellie.Bot.Db/Models/filter/FilteredWord.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/BanTemplate.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/MutedUserId.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/PunishmentAction.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/WarnExpireAction.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/Warning.cs delete mode 100644 src/Ellie.Bot.Db/Models/punish/WarningPunishment.cs delete mode 100644 src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredRole.cs delete mode 100644 src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredUser.cs delete mode 100644 src/Ellie.Bot.Db/Models/support/PatronQuota.cs delete mode 100644 src/Ellie.Bot.Db/Models/support/RewardUser.cs delete mode 100644 src/Ellie.Bot.Db/Models/untimer/UnbanTimer.cs delete mode 100644 src/Ellie.Bot.Db/Models/untimer/UnmuteTimer.cs delete mode 100644 src/Ellie.Bot.Db/Models/untimer/UnroleTimer.cs delete mode 100644 src/Ellie.Bot.Db/Models/xp/UserXpStats.cs delete mode 100644 src/Ellie.Bot.Db/Models/xp/XpSettings.cs delete mode 100644 src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs delete mode 100644 src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs delete mode 100644 src/Ellie.Bot.Generators.Cloneable/SyntaxReceiver.cs delete mode 100644 src/Ellie.Bot.Generators.Strings/LocalizedStringsGenerator.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Administration.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/AdministrationService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/AutoPublishService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommandsService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GreetBye/GreetCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GreetBye/GreetGrouper.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GreetBye/GreetService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/GreetBye/GreetSettings.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/ImageOnlyChannelService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/LocalizationCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Mute/MuteCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Mute/MuteService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/PermOverrides/DiscordPermOverrideCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/PrefixCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Protection/ProtectionCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Protection/ProtectionService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Protection/ProtectionStats.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Protection/PunishQueueItem.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Protection/UserSpamStats.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Prune/PruneCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Prune/PruneService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Role/IReactionRoleService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Role/ReactionRoleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Role/ReactionRolesService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Role/RoleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Self/CheckForUpdatesService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Self/SelfCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Self/SelfService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/ServerLog/DummyLogCommandService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommandService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/Timezone/TimeZoneCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishService.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/VcRole/VcRoleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Administration/VcRole/VcRoleService.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/EllieExpressionsExtensios.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/ExprField.cs delete mode 100644 src/Ellie.Bot.Modules.Expresssions/TypeReaders/CommandOrExprTypeReader.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRace.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRaceService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingUser.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyJoinedException.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyStartedException.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AnimalRaceFullException.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/NotEnoughFundsException.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/RaceOptions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Blackjack.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Player.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/CleanupCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4Commands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/DiceRoll/DiceRollCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Draw/DrawCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/EconomyResult.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/EventOptions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/GameStatusEvent.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/ICurrencyEvent.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Events/ReactionEvent.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipCoinCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipResult.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Gambling.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfig.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfigService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/GamblingService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/GlobalUsings.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/InputRpsPick.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantAndPickCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantPickService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleGame.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Shop/IShopService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Slot/SlotCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/VoteRewardService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuClaimCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/AffinityTitle.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/ClaimTitle.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/DivorceResult.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/Extensions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/WaifuClaimResult.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/Waifu.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuInfoStats.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuItem.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuLbResult.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdate.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdateType.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/Decks/QuadDeck.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingCleanupService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/NewGamblingService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/RollDuelGame.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/BaseShmartInputAmountReader.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartBankInputAmountReader.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartNumberTypeReader.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/Acrophobia.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcrophobiaUser.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcropobiaCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/CleverBotCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/CleverbotResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/Gpt3Response.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/IChatterBotSession.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialCleverbotSession.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialGpt3Session.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Games.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/GamesConfig.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/GamesConfigService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/GamesService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/GirlRating.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/DefaultHangmanSource.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanGame.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanTerm.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanSource.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Nunchi/Nunchi.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Nunchi/NunchiCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Polls/PollCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Polls/PollExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Polls/PollRunner.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Polls/PollService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/SpeedTypingCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingArticle.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingGame.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToe.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToeCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/Games.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/DefaultQuestionPool.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/IQuestionPool.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/PokemonQuestionPool.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGame.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGamesService.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaOptions.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaQuestion.cs delete mode 100644 src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaUser.cs delete mode 100644 src/Ellie.Bot.Modules.Help/CommandJsonObject.cs delete mode 100644 src/Ellie.Bot.Modules.Help/CommandsOptions.cs delete mode 100644 src/Ellie.Bot.Modules.Help/Help.cs delete mode 100644 src/Ellie.Bot.Modules.Help/HelpService.cs delete mode 100644 src/Ellie.Bot.Modules.Music/CleanupCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Music.cs delete mode 100644 src/Ellie.Bot.Modules.Music/PlaylistCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/AyuVoiceStateService.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/IMusicService.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/MusicService.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/SoundCloudApiService.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/extractor/Misc.cs delete mode 100644 src/Ellie.Bot.Modules.Music/Services/extractor/YtLoader.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ICachableTrackData.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ILocalTrackResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IMusicPlayer.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IMusicQueue.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IPlatformQueryResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IQueuedTrackInfo.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IRadioResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ISoundcloudResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ITrackCacher.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ITrackInfo.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/ITrackResolveProvider.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IVoiceProxy.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/IYoutubeResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/CachableTrackData.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/MultimediaTimer.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/MusicExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlatform.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlayer.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/MusicQueue.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/RemoteTrackInfo.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/SimpleTrackInfo.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/TrackCacher.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Impl/VoiceProxy.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Resolvers/LocalTrackResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Resolvers/RadioResolveStrategy.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Resolvers/SoundcloudResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Resolvers/TrackResolveProvider.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/Resolvers/YtdlYoutubeResolver.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/db/MusicPlayerSettingsExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylist.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylistExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Music/_common/db/MusicSettings.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Config/PatronageConfig.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/CurrencyRewardService.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/InsufficientTier.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonClient.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonCredentials.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonData.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonMemberData.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonRefreshData.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonSubscriptionHandler.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/PatronageCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Patronage/Patronage/PatronageService.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/Blacklist/BlacklistCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdService.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdsCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/Filter/FilterCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/Filter/FilterService.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/Filter/ServerFilterSettings.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionService.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/PermissionCache.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/PermissionExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/Permissions.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/PermissionsCollection.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/PermissionsService.cs delete mode 100644 src/Ellie.Bot.Modules.Permissions/ResetPermissionsCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Anime/AnimeResult.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Anime/MangaResult.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/CryptoCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/CryptoService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Drawing/CandleDrawingData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Drawing/IStockChartDrawingService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Drawing/ImagesharpStockChartDrawingService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/IStockDataService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResult.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonApiClient.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonStockDataService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/CandleData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/ImageData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/QuoteResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/StockData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/SymbolData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceCandleData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponseItem.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooQueryModel.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Feeds/FeedCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Feeds/FeedsService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/JokeCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/MemegenCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/OsuCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/PathOfExileCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/PlaceCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/PokemonSearchCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Searches.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/SearchesService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/StreamNotification/StreamOnlineMessageDeleterService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Translate/ITranslateService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Translate/TranslateService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/Translate/TranslatorCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/XkcdCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtTrackService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtUploadCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/AltExtensions.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/BibleVerses.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Config/ImgSearchEngine.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfig.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfigService.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Config/WebSearchEngine.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/CryptoData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/DefineModel.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/E621Object.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Exceptions/StreamNotFoundException.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Extensions.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/Gallery.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/GatariUserResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/GatariUserStatsResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/GoogleSearchResult.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/HearthstoneCardData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/LowerCaseNamingPolicy.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/MagicItem.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/MtgData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/NovelData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/OmdbMovie.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/OsuUserData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/PathOfExileModels.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/SteamGameId.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/HelixStreamsResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/HelixUsersResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/PicartoChannelResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/StreamData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/StreamDataKey.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/TrovoGetUsersResponse.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/TrovoRequestData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/TrovoSocialLink.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/TwitchResponseV5.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Models/TwitchUsersResponseV5.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/NotifChecker.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Providers/PicartoProvider.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Providers/Provider.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Providers/TrovoProvider.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/StreamNotifications/Providers/TwitchHelixProvider.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/TimeData.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/TimeModels.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/UrbanDef.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/WeatherModels.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/WikipediaApiModel.cs delete mode 100644 src/Ellie.Bot.Modules.Searches/_common/WoWJoke.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Alias/AliasCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Alias/AliasService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Calc/CalcCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/ConfigCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Info/InfoCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Info/InviteCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Invite/InviteService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Quote/IQuoteService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Quote/QuoteCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Quote/QuoteService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Remind/RemindCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Remind/RemindService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Repeater/RepeatCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Repeater/RepeaterService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Repeater/RunningRepeater.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/StreamRole/StreamRoleCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/StreamRole/StreamRoleService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/UnitConversion/ConverterService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/UnitConversion/UnitConversionCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/Utility.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/VerboseErrors/EvalCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/VerboseErrors/EvalGlobals.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/VerboseErrorsService.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/_common/ConvertUnit.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/_common/Exceptions/StreamRoleNotFoundException.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/_common/Exceptions/StreamRolePermissionException.cs delete mode 100644 src/Ellie.Bot.Modules.Utility/_common/StreamRoleListType.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/CleanupCommands.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Club.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/ClubService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/IClubService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubAcceptResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubBanResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubCreateResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubKickResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubLeaveResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubTransferError.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ClubUnbanResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/SetClubIconResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Club/Results/ToggleAdminResult.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/Xp.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/XpConfig.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/XpConfigService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/XpRewards.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/XpService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/Extensions.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/FullUserStats.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/IXpCleanupService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/UserCacheItem.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/XpCleanupService.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/XpTemplate.cs delete mode 100644 src/Ellie.Bot.Modules.Xp/_common/db/XpShopOwnedItem.cs delete mode 100644 src/Ellie.Coordinator/CoordStartup.cs delete mode 100644 src/Ellie.Coordinator/LogSetup.cs delete mode 100644 src/Ellie.Coordinator/Program.cs delete mode 100644 src/Ellie.Coordinator/Properties/launchSettings.json delete mode 100644 src/Ellie.Coordinator/Protos/coordinator.proto delete mode 100644 src/Ellie.Coordinator/Services/CoordinatorRunner.cs delete mode 100644 src/Ellie.Coordinator/Services/CoordinatorService.cs delete mode 100644 src/Ellie.Coordinator/Shared/Config.cs delete mode 100644 src/Ellie.Coordinator/Shared/CoordState.cs delete mode 100644 src/Ellie.Coordinator/Shared/JsonStatusObject.cs delete mode 100644 src/Ellie.Coordinator/Shared/ShardStatus.cs delete mode 100644 src/Ellie.Coordinator/appsettings.Development.json delete mode 100644 src/Ellie.Coordinator/appsettings.json delete mode 100644 src/Ellie.Coordinator/coord.yml delete mode 100644 src/Ellie.Econ/Deck/Deck.cs delete mode 100644 src/Ellie.Econ/Deck/NewCard.cs delete mode 100644 src/Ellie.Econ/Deck/NewDeck.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/MultipleRegularDeck/MultipleRegularDeck.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/RegularCard.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/RegularDeck.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/RegularDeckExtensions.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/RegularSuit.cs delete mode 100644 src/Ellie.Econ/Deck/Regular/RegularValue.cs delete mode 100644 src/Ellie.Econ/Gambling/Betdraw/BetdrawColorGuess.cs delete mode 100644 src/Ellie.Econ/Gambling/Betdraw/BetdrawGame.cs delete mode 100644 src/Ellie.Econ/Gambling/Betdraw/BetdrawResult.cs delete mode 100644 src/Ellie.Econ/Gambling/Betdraw/BetdrawResultType.cs delete mode 100644 src/Ellie.Econ/Gambling/Betdraw/BetdrawValueGuess.cs delete mode 100644 src/Ellie.Econ/Gambling/Betflip/BetflipGame.cs delete mode 100644 src/Ellie.Econ/Gambling/Betflip/BetflipResult.cs delete mode 100644 src/Ellie.Econ/Gambling/Betroll/BetrollGame.cs delete mode 100644 src/Ellie.Econ/Gambling/Betroll/BetrollResult.cs delete mode 100644 src/Ellie.Econ/Gambling/Rps/RpsGame.cs delete mode 100644 src/Ellie.Econ/Gambling/Slot/SlotGame.cs delete mode 100644 src/Ellie.Econ/Gambling/Slot/SlotResult.cs delete mode 100644 src/Ellie.Econ/Gambling/Wof/LulaResult.cs delete mode 100644 src/Ellie.Econ/Gambling/Wof/WofGame.cs create mode 100644 src/Ellie.Tests/Ellie.Tests.csproj create mode 100644 src/Ellie.Tests/README.md delete mode 100644 src/Ellie.VotesApi/.dockerignore delete mode 100644 src/Ellie.VotesApi/.gitignore delete mode 100644 src/Ellie.VotesApi/Common/AuthHandler.cs delete mode 100644 src/Ellie.VotesApi/Common/ConfKeys.cs delete mode 100644 src/Ellie.VotesApi/Common/DiscordsVoteWebhookModel.cs delete mode 100644 src/Ellie.VotesApi/Common/Policies.cs delete mode 100644 src/Ellie.VotesApi/Common/ToppVoteWebhookModel.cs delete mode 100644 src/Ellie.VotesApi/Controllers/DiscordsController.cs delete mode 100644 src/Ellie.VotesApi/Controllers/TopGgController.cs delete mode 100644 src/Ellie.VotesApi/Controllers/WebhookController.cs delete mode 100644 src/Ellie.VotesApi/Dockerfile delete mode 100644 src/Ellie.VotesApi/Program.cs delete mode 100644 src/Ellie.VotesApi/Properties/launchSettings.json delete mode 100644 src/Ellie.VotesApi/Services/FileVotesCache.cs delete mode 100644 src/Ellie.VotesApi/Services/IVotesCache.cs delete mode 100644 src/Ellie.VotesApi/Startup.cs delete mode 100644 src/Ellie.VotesApi/WeatherForecast.cs delete mode 100644 src/Ellie.VotesApi/appsettings.Development.json delete mode 100644 src/Ellie.VotesApi/appsettings.json delete mode 100644 src/Ellie/Bot.cs delete mode 100644 src/Ellie/Db/EllieContext.cs delete mode 100644 src/Ellie/Db/EllieDbService.cs delete mode 100644 src/Ellie/Db/MysqlContext.cs delete mode 100644 src/Ellie/Db/PostgreSqlContext.cs delete mode 100644 src/Ellie/Db/SqliteContext.cs delete mode 100644 src/Ellie/Marmalade/Adapters/BehaviorAdapter.cs delete mode 100644 src/Ellie/Marmalade/Adapters/ContextAdapterFactory.cs delete mode 100644 src/Ellie/Marmalade/Adapters/DmContextAdapter.cs delete mode 100644 src/Ellie/Marmalade/Adapters/FilterAdapter.cs delete mode 100644 src/Ellie/Marmalade/Adapters/GuildContextAdapter.cs delete mode 100644 src/Ellie/Marmalade/Adapters/ParamParserAdapter.cs delete mode 100644 src/Ellie/Marmalade/CommandContextType.cs delete mode 100644 src/Ellie/Marmalade/Config/IMarmaladeConfigService.cs delete mode 100644 src/Ellie/Marmalade/Config/MarmaladeConfig.cs delete mode 100644 src/Ellie/Marmalade/Config/MarmaladeConfigService.cs delete mode 100644 src/Ellie/Marmalade/MarmaladeAssemblyLoadContext.cs delete mode 100644 src/Ellie/Marmalade/MarmaladeIoCKernelModule.cs delete mode 100644 src/Ellie/Marmalade/MarmaladeLoaderService.cs delete mode 100644 src/Ellie/Marmalade/Models/CanaryCommandData.cs delete mode 100644 src/Ellie/Marmalade/Models/CanaryData.cs delete mode 100644 src/Ellie/Marmalade/Models/ParamData.cs delete mode 100644 src/Ellie/Marmalade/Models/ResolvedMarmalade.cs delete mode 100644 src/Ellie/Marmalade/RemovablePlanner.cs delete mode 100644 src/Ellie/Migrations/MigrationQueries.cs delete mode 100644 src/Ellie/Modules/Gambling/CurrencyProvider.cs delete mode 100644 src/Ellie/Modules/Marmalades/IMarmaladesRepositoryService.cs delete mode 100644 src/Ellie/Modules/Marmalades/Marmalade.cs delete mode 100644 src/Ellie/Modules/Marmalades/MarmaladeItem.cs delete mode 100644 src/Ellie/Modules/Marmalades/MarmaladesRepositoryService.cs delete mode 100644 src/Ellie/PermissionChecker.cs delete mode 100644 src/Ellie/Program.cs delete mode 100644 src/Ellie/Services/Impl/BotCredsProvider.cs delete mode 100644 src/Ellie/Services/Impl/GoogleApiService.cs delete mode 100644 src/Ellie/Services/Impl/GoogleApiService_SupportedLanguages.cs delete mode 100644 src/Ellie/Services/Impl/ImageCache.cs delete mode 100644 src/Ellie/Services/Impl/LocalDataCache.cs delete mode 100644 src/Ellie/Services/Impl/Localization.cs delete mode 100644 src/Ellie/Services/Impl/PubSub/JsonSeria.cs delete mode 100644 src/Ellie/Services/Impl/PubSub/RedisPubSub.cs delete mode 100644 src/Ellie/Services/Impl/PubSub/YamlSeria.cs delete mode 100644 src/Ellie/Services/Impl/RedisBotCache.cs delete mode 100644 src/Ellie/Services/Impl/RedisBotStringsProvider.cs delete mode 100644 src/Ellie/Services/Impl/RemoteGrpcCoordinator.cs delete mode 100644 src/Ellise.Common/AsyncLazy.cs delete mode 100644 src/Ellise.Common/Cache/BotCacheExtensions.cs delete mode 100644 src/Ellise.Common/Cache/IBotCache.cs delete mode 100644 src/Ellise.Common/Cache/MemoryBotCache.cs delete mode 100644 src/Ellise.Common/Collections/ConcurrentHashSet.cs delete mode 100644 src/Ellise.Common/Collections/IndexedCollection.cs delete mode 100644 src/Ellise.Common/EllieRandom.cs delete mode 100644 src/Ellise.Common/Extensions/ArrayExtensions.cs delete mode 100644 src/Ellise.Common/Extensions/EnumerableExtensions.cs delete mode 100644 src/Ellise.Common/Extensions/Extensions.cs delete mode 100644 src/Ellise.Common/Extensions/HttpClientExtensions.cs delete mode 100644 src/Ellise.Common/Extensions/OneOfExtensions.cs delete mode 100644 src/Ellise.Common/Extensions/PipeExtensions.cs delete mode 100644 src/Ellise.Common/Extensions/StringExtensions.cs delete mode 100644 src/Ellise.Common/Helpers/LogSetup.cs delete mode 100644 src/Ellise.Common/Helpers/StandardConversions.cs delete mode 100644 src/Ellise.Common/Kwum.cs delete mode 100644 src/Ellise.Common/PubSub/EventPubSub.cs delete mode 100644 src/Ellise.Common/PubSub/IPubSub.cs delete mode 100644 src/Ellise.Common/PubSub/ISeria.cs delete mode 100644 src/Ellise.Common/QueueRunner.cs delete mode 100644 src/Ellise.Common/TypedKey.cs delete mode 100644 src/Ellise.Common/YamlHelper.cs diff --git a/src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs b/src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs deleted file mode 100644 index 19c5b52..0000000 --- a/src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable disable -namespace Ellie; - -public interface IBotCredentials -{ - string Token { get; } - string GoogleApiKey { get; } - ICollection OwnerIds { get; set; } - bool UsePrivilegedIntents { get; } - string RapidApiKey { get; } - - Creds.DbOptions Db { get; } - string OsuApiKey { get; } - int TotalShards { get; } - Creds.PatreonSettings Patreon { get; } - string CleverbotApiKey { get; } - string Gpt3ApiKey { get; } - RestartConfig RestartCommand { get; } - Creds.VotesSettings Votes { get; } - string BotListToken { get; } - string RedisOptions { get; } - string LocationIqApiKey { get; } - string TimezoneDbApiKey { get; } - string CoinmarketcapApiKey { get; } - string TrovoClientId { get; } - string CoordinatorUrl { get; set; } - string TwitchClientId { get; set; } - string TwitchClientSecret { get; set; } - GoogleApiConfig Google { get; set; } - BotCacheImplemenation BotCache { get; set; } -} - -public interface IVotesSettings -{ - string TopggServiceUrl { get; set; } - string TopggKey { get; set; } - string DiscordsServiceUrl { get; set; } - string DiscordsKey { get; set; } -} - -public interface IPatreonSettings -{ - public string ClientId { get; set; } - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string ClientSecret { get; set; } - public string CampaignId { get; set; } -} - -public interface IRestartConfig -{ - string Cmd { get; set; } - string Args { get; set; } -} - -public class RestartConfig : IRestartConfig -{ - public string Cmd { get; set; } - public string Args { get; set; } -} - -public enum BotCacheImplemenation -{ - Memory, - Redis -} - -public interface IDbOptions -{ - string Type { get; set; } - string ConnectionString { get; set; } -} - -public interface IGoogleApiConfig -{ - string SearchId { get; init; } - string ImageSearchId { get; init; } -} diff --git a/src/Ellie.Bot.Common/Abstractions/creds/IBotCredsProvider.cs b/src/Ellie.Bot.Common/Abstractions/creds/IBotCredsProvider.cs deleted file mode 100644 index a37ad61..0000000 --- a/src/Ellie.Bot.Common/Abstractions/creds/IBotCredsProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie; - -public interface IBotCredsProvider -{ - public void Reload(); - public IBotCredentials GetCreds(); - public void ModifyCredsFile(Action func); -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs b/src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs deleted file mode 100644 index 5d25b5f..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using YamlDotNet.Serialization; - -namespace Ellie.Services; - -public sealed class CommandStrings -{ - [YamlMember(Alias = "desc")] - public string Desc { get; set; } - - [YamlMember(Alias = "args")] - public string[] Args { get; set; } -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs b/src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs deleted file mode 100644 index 76e280a..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -using System.Globalization; - -namespace Ellie.Common; - -/// -/// Defines methods to retrieve and reload bot strings -/// -public interface IBotStrings -{ - string GetText(string key, ulong? guildId = null, params object[] data); - string GetText(string key, CultureInfo locale, params object[] data); - void Reload(); - CommandStrings GetCommandStrings(string commandName, ulong? guildId = null); - CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo); -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsExtensions.cs b/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsExtensions.cs deleted file mode 100644 index b0e0ab8..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -using System.Globalization; - -namespace Ellie.Common; - -public static class BotStringsExtensions -{ - // this one is for pipe fun, see PipeExtensions.cs - public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId) - => strings.GetText(str.Key, guildId, str.Params); - - public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null) - => strings.GetText(str.Key, guildId, str.Params); - - public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture) - => strings.GetText(str.Key, culture, str.Params); -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsProvider.cs b/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsProvider.cs deleted file mode 100644 index 88181e1..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/IBotStringsProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -/// -/// Implemented by classes which provide localized strings in their own ways -/// -public interface IBotStringsProvider -{ - /// - /// Gets localized string - /// - /// Language name - /// String key - /// Localized string - string GetText(string localeName, string key); - - /// - /// Reloads string cache - /// - void Reload(); - - /// - /// Gets command arg examples and description - /// - /// Language name - /// Command name - CommandStrings GetCommandStrings(string localeName, string commandName); -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs b/src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs deleted file mode 100644 index e163bdf..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable - -namespace Ellie.Services; - -/// -/// Basic interface used for classes implementing strings loading mechanism -/// -public interface IStringsSource -{ - /// - /// Gets all response strings - /// - /// Dictionary(localename, Dictionary(key, response)) - Dictionary> GetResponseStrings(); - - Dictionary> GetCommandStrings(); -} diff --git a/src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs b/src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs deleted file mode 100644 index 2d737c0..0000000 --- a/src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ellie; - -public readonly struct LocStr -{ - public readonly string Key; - public readonly object[] Params; - - public LocStr(string key, params object[] data) - { - Key = key; - Params = data; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs b/src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs deleted file mode 100644 index 8745c7e..0000000 --- a/src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Ellie.Common.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public sealed class AliasesAttribute : AliasAttribute -{ - public AliasesAttribute([CallerMemberName] string memberName = "") - : base(CommandNameLoadHelper.GetAliasesFor(memberName)) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/CmdAttribute.cs b/src/Ellie.Bot.Common/Attributes/CmdAttribute.cs deleted file mode 100644 index 4e60afe..0000000 --- a/src/Ellie.Bot.Common/Attributes/CmdAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Ellie.Common.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public sealed class CmdAttribute : CommandAttribute -{ - public string MethodName { get; } - - public CmdAttribute([CallerMemberName] string memberName = "") - : base(CommandNameLoadHelper.GetCommandNameFor(memberName)) - { - MethodName = memberName.ToLowerInvariant(); - Aliases = CommandNameLoadHelper.GetAliasesFor(memberName); - Remarks = memberName.ToLowerInvariant(); - Summary = memberName.ToLowerInvariant(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs b/src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs deleted file mode 100644 index 2a24a1d..0000000 --- a/src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -/// -/// Classed marked with this attribute will not be added to the service provider -/// -[AttributeUsage(AttributeTargets.Class)] -public class DIIgnoreAttribute : Attribute -{ - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs b/src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs deleted file mode 100644 index d4a51c4..0000000 --- a/src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Common.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public sealed class EllieOptionsAttribute : Attribute - where TOption: IEllieCommandOptions -{ -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs b/src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs deleted file mode 100644 index d1a0b75..0000000 --- a/src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable disable -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Common; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -[SuppressMessage("Style", "IDE0022:Use expression body for methods")] -public sealed class NoPublicBotAttribute : PreconditionAttribute -{ - public override Task CheckPermissionsAsync( - ICommandContext context, - CommandInfo command, - IServiceProvider services) - { -#if GLOBAL_ELLIE - return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://docs.elliebot.net).")); -#else - return Task.FromResult(PreconditionResult.FromSuccess()); -#endif - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs b/src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs deleted file mode 100644 index b706c28..0000000 --- a/src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable disable -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Common; - -[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_ELLIE || 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/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs b/src/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs deleted file mode 100644 index 007238e..0000000 --- a/src/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Ellie.Common.Attributes; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -public sealed class OwnerOnlyAttribute : PreconditionAttribute -{ - public override Task CheckPermissionsAsync( - ICommandContext context, - CommandInfo command, - IServiceProvider services) - { - var creds = services.GetRequiredService().GetCreds(); - - return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id - ? PreconditionResult.FromSuccess() - : PreconditionResult.FromError("Not owner")); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs b/src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs deleted file mode 100644 index cfd5a08..0000000 --- a/src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Ellie.Common.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public sealed class RatelimitAttribute : PreconditionAttribute -{ - public int Seconds { get; } - - public RatelimitAttribute(int seconds) - { - if (seconds <= 0) - throw new ArgumentOutOfRangeException(nameof(seconds)); - - Seconds = seconds; - } - - public override async Task CheckPermissionsAsync( - ICommandContext context, - CommandInfo command, - IServiceProvider services) - { - if (Seconds == 0) - return PreconditionResult.FromSuccess(); - - var cache = services.GetRequiredService(); - var rem = await cache.GetRatelimitAsync( - new($"precondition:{context.User.Id}:{command.Name}"), - Seconds.Seconds()); - - if (rem is null) - return PreconditionResult.FromSuccess(); - - var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s."; - - return PreconditionResult.FromError(msgContent); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs b/src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs deleted file mode 100644 index 2e0af03..0000000 --- a/src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Discord; - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public class UserPermAttribute : RequireUserPermissionAttribute -{ - public UserPermAttribute(GuildPerm permission) - : base(permission) - { - } - - public UserPermAttribute(ChannelPerm permission) - : base(permission) - { - } - - public override Task CheckPermissionsAsync( - ICommandContext context, - CommandInfo command, - IServiceProvider services) - { - var permService = services.GetRequiredService(); - if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _)) - return Task.FromResult(PreconditionResult.FromSuccess()); - - return base.CheckPermissionsAsync(context, command, services); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/BotCommandTypeReader.cs b/src/Ellie.Bot.Common/BotCommandTypeReader.cs deleted file mode 100644 index d4f70d1..0000000 --- a/src/Ellie.Bot.Common/BotCommandTypeReader.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class CommandTypeReader : EllieTypeReader -{ - private readonly CommandService _cmds; - private readonly ICommandHandler _handler; - - public CommandTypeReader(ICommandHandler handler, CommandService cmds) - { - _handler = handler; - _cmds = cmds; - } - - public override ValueTask> ReadAsync(ICommandContext ctx, string input) - { - input = input.ToUpperInvariant(); - var prefix = _handler.GetPrefix(ctx.Guild); - if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture)) - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - - input = input[prefix.Length..]; - - var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input)); - if (cmd is null) - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - - return new(TypeReaderResult.FromSuccess(cmd)); - } -} diff --git a/src/Ellie.Bot.Common/CleanupModuleBase.cs b/src/Ellie.Bot.Common/CleanupModuleBase.cs deleted file mode 100644 index d7a77b8..0000000 --- a/src/Ellie.Bot.Common/CleanupModuleBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public abstract class CleanupModuleBase : EllieModule -{ - protected async Task ConfirmActionInternalAsync(string name, Func action) - { - try - { - var embed = _eb.Create() - .WithTitle(GetText(strs.sql_confirm_exec)) - .WithDescription(name); - - if (!await PromptUserConfirmAsync(embed)) - return; - - await action(); - await ctx.OkAsync(); - } - catch (Exception ex) - { - await SendErrorAsync(ex.ToString()); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/CleaverBotResponseStr.cs b/src/Ellie.Bot.Common/CleaverBotResponseStr.cs deleted file mode 100644 index 95b0a14..0000000 --- a/src/Ellie.Bot.Common/CleaverBotResponseStr.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -using System.Runtime.InteropServices; - -namespace Ellie.Modules.Permissions; - -[StructLayout(LayoutKind.Sequential, Size = 1)] -public readonly struct CleverBotResponseStr -{ - public const string CLEVERBOT_RESPONSE = "cleverbot:response"; -} diff --git a/src/Ellie.Bot.Common/CommandNameLoadHelper.cs b/src/Ellie.Bot.Common/CommandNameLoadHelper.cs deleted file mode 100644 index b963de6..0000000 --- a/src/Ellie.Bot.Common/CommandNameLoadHelper.cs +++ /dev/null @@ -1,31 +0,0 @@ -using YamlDotNet.Serialization; - -namespace Ellie.Common.Attributes; - -public static class CommandNameLoadHelper -{ - private static readonly IDeserializer _deserializer = new Deserializer(); - - private static readonly Lazy> _lazyCommandAliases - = new(() => LoadAliases()); - - public static Dictionary LoadAliases(string aliasesFilePath = "data/aliases.yml") - { - var text = File.ReadAllText(aliasesFilePath); - return _deserializer.Deserialize>(text); - } - - public static string[] GetAliasesFor(string methodName) - => _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 - ? aliases.Skip(1).ToArray() - : Array.Empty(); - - public static string GetCommandNameFor(string methodName) - { - methodName = methodName.ToLowerInvariant(); - var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 - ? aliases[0] - : methodName; - return toReturn; - } -} diff --git a/src/Ellie.Bot.Common/Configs/BotConfig.cs b/src/Ellie.Bot.Common/Configs/BotConfig.cs deleted file mode 100644 index 3fd22b1..0000000 --- a/src/Ellie.Bot.Common/Configs/BotConfig.cs +++ /dev/null @@ -1,203 +0,0 @@ -#nullable disable - -using Cloneable; -using Ellie.Common.Yml; -using SixLabors.ImageSharp.PixelFormats; -using System.Globalization; -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Ellie.Common.Configs; - -[Cloneable] -public sealed partial class BotConfig : ICloneable -{ - [Comment("""DO NOT CHANGE""")] - public int Version { get; set; } = 5; - - [Comment(""" - Most commands, when executed, have a small colored line - next to the response. The color depends whether the command - is completed, errored or in progress (pending) - Color settings below are for the color of those lines. - To get color's hex, you can go here https://htmlcolorcodes.com/ - and copy the hex code fo your selected color (marked as #) - """)] - public ColorConfig Color { get; set; } - - [Comment("Default bot language. It has to be in the list of supported languages (.langli)")] - public CultureInfo DefaultLocale { get; set; } - - [Comment(""" - Style in which executed commands will show up in the console. - Allowed values: Simple, Normal, None - """)] - public ConsoleOutputType ConsoleOutputType { get; set; } - - [Comment("""Whether the bot will check for new releases every hour""")] - public bool CheckForUpdates { get; set; } = true; - - [Comment("""Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?""")] - public bool ForwardMessages { get; set; } - - [Comment(""" - Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml), - or all owners? (this might cause the bot to lag if there's a lot of owners specified) - """)] - public bool ForwardToAllOwners { get; set; } - - [Comment(""" - Any messages sent by users in Bot's DM to be forwarded to the specified channel. - This option will only work when ForwardToAllOwners is set to false - """)] - public ulong? ForwardToChannel { get; set; } - - [Comment(""" - When a user DMs the bot with a message which is not a command - they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot. - Supports embeds. How it looks: https://puu.sh/B0BLV.png - """)] - [YamlMember(ScalarStyle = ScalarStyle.Literal)] - public string DmHelpText { get; set; } - - [Comment(""" - Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response. - Case insensitive. - Leave empty to reply with DmHelpText to every DM. - """)] - public List DmHelpTextKeywords { get; set; } - - [Comment("""This is the response for the .h command""")] - [YamlMember(ScalarStyle = ScalarStyle.Literal)] - public string HelpText { get; set; } - - [Comment("""List of modules and commands completely blocked on the bot""")] - public BlockedConfig Blocked { get; set; } - - [Comment("""Which string will be used to recognize the commands""")] - public string Prefix { get; set; } - - [Comment(""" - 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. - 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. - """)] - public bool GroupGreets { get; set; } - - [Comment(""" - Whether the bot will rotate through all specified statuses. - This setting can be changed via .ropl command. - See RotatingStatuses submodule in Administration. - """)] - public bool RotateStatuses { get; set; } - - public BotConfig() - { - var color = new ColorConfig(); - Color = color; - DefaultLocale = new("en-US"); - ConsoleOutputType = ConsoleOutputType.Normal; - ForwardMessages = false; - ForwardToAllOwners = false; - DmHelpText = """{"description": "Type `%prefix%h` for help."}"""; - HelpText = """ - { - "title": "To invite me to your server, use this link", - "description": "https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303", - "color": 53380, - "thumbnail": "https://i.imgur.com/nKYyqMK.png", - "fields": [ - { - "name": "Useful help commands", - "value": "`%bot.prefix%modules` Lists all bot modules. - `%prefix%h CommandName` Shows some help about a specific command. - `%prefix%commands ModuleName` Lists all commands in a module.", - "inline": false - }, - { - "name": "List of all Commands", - "value": "https://commands.elliebot.net", - "inline": false - }, - { - "name": "Ellie Support Server", - "value": "https://discord.elliebot.net/ ", - "inline": true - } - ] - } - """; - var blocked = new BlockedConfig(); - Blocked = blocked; - Prefix = "."; - RotateStatuses = false; - GroupGreets = false; - DmHelpTextKeywords = new() - { - "help", - "commands", - "cmds", - "module", - "can you do" - }; - } - - // [Comment(@"Whether the prefix will be a suffix, or prefix. - // For example, if your prefix is ! you will run a command called 'cash' by typing either - // '!cash @Someone' if your prefixIsSuffix: false or - // 'cash @Someone!' if your prefixIsSuffix: true")] - // public bool PrefixIsSuffix { get; set; } - - // public string Prefixed(string text) => PrefixIsSuffix - // ? text + Prefix - // : Prefix + text; - - public string Prefixed(string text) - => Prefix + text; -} - -[Cloneable] -public sealed partial class BlockedConfig -{ - public HashSet Commands { get; set; } - public HashSet Modules { get; set; } - - public BlockedConfig() - { - Modules = new(); - Commands = new(); - } -} - -[Cloneable] -public partial class ColorConfig -{ - [Comment("""Color used for embed responses when command successfully executes""")] - public Rgba32 Ok { get; set; } - - [Comment("""Color used for embed responses when command has an error""")] - public Rgba32 Error { get; set; } - - [Comment("""Color used for embed responses while command is doing work or is in progress""")] - public Rgba32 Pending { get; set; } - - public ColorConfig() - { - Ok = Rgba32.ParseHex("00e584"); - Error = Rgba32.ParseHex("ee281f"); - Pending = Rgba32.ParseHex("faa61a"); - } -} - -public enum ConsoleOutputType -{ - Normal = 0, - Simple = 1, - None = 2 -} diff --git a/src/Ellie.Bot.Common/Configs/IConfigSeria.cs b/src/Ellie.Bot.Common/Configs/IConfigSeria.cs deleted file mode 100644 index 4aea72e..0000000 --- a/src/Ellie.Bot.Common/Configs/IConfigSeria.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Ellie.Common.Configs; - -/// -/// Base interface for available config serializers -/// -public interface IConfigSeria -{ - /// - /// Serialize the object to string - /// - public string Serialize(T obj) - where T : notnull; - - /// - /// Deserialize string data into an object of the specified type - /// - public T Deserialize(string data); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Creds.cs b/src/Ellie.Bot.Common/Creds.cs deleted file mode 100644 index 750277c..0000000 --- a/src/Ellie.Bot.Common/Creds.cs +++ /dev/null @@ -1,272 +0,0 @@ -#nullable disable -using Ellie.Common.Yml; - -namespace Ellie.Common; - -public sealed class Creds : IBotCredentials -{ - [Comment("""DO NOT CHANGE""")] - public int Version { get; set; } - - [Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")] - public string Token { get; set; } - - [Comment(""" - List of Ids of the users who have bot owner permissions - **DO NOT ADD PEOPLE YOU DON'T TRUST** - """)] - public ICollection OwnerIds { get; set; } - - [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 be running on. - Leave at 1 if you don't know what you're doing. - - note: If you are planning to have more than one shard, then you must change botCache to 'redis'. - Also, in that case you should be using Ellie.Coordinator to start the bot, and it will correctly override this value. - """)] - public int TotalShards { get; set; } - - [Comment( - """ - 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). - """)] - 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 Ellie.""")] - public VotesSettings Votes { get; set; } - - [Comment(""" - Patreon auto reward system settings. - go to https://www.patreon.com/portal -> my clients -> create client - """)] - public PatreonSettings Patreon { get; set; } - - [Comment("""Api key for sending stats to DiscordBotList.""")] - public string BotListToken { get; set; } - - [Comment("""Official cleverbot api key.""")] - public string CleverbotApiKey { get; set; } - - [Comment(@"Official GPT-3 api key.")] - public string Gpt3ApiKey { get; set; } - - [Comment(""" - Which cache implementation should bot use. - 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. - 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml - """)] - public BotCacheImplemenation BotCache { get; set; } - - [Comment(""" - Redis connection string. Don't change if you don't know what you're doing. - Only used if botCache is set to 'redis' - """)] - public string RedisOptions { get; set; } - - [Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")] - public DbOptions Db { get; set; } - - [Comment(""" - Address and port of the coordinator endpoint. Leave empty for default. - Change only if you've changed the coordinator address or port. - """)] - public string CoordinatorUrl { get; set; } - - [Comment( - """Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)""")] - public string RapidApiKey { get; set; } - - [Comment(""" - https://locationiq.com api key (register and you will receive the token in the email). - Used only for .time command. - """)] - public string LocationIqApiKey { get; set; } - - [Comment(""" - https://timezonedb.com api key (register and you will receive the token in the email). - Used only for .time command - """)] - public string TimezoneDbApiKey { get; set; } - - [Comment(""" - https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. - Used for cryptocurrency related commands. - """)] - public string CoinmarketcapApiKey { get; set; } - -// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute. -// Used for stocks related commands.")] -// public string PolygonIoApiKey { get; set; } - - [Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")] - public string OsuApiKey { get; set; } - - [Comment(""" - Optional Trovo client id. - You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors. - """)] - public string TrovoClientId { get; set; } - - [Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")] - public string TwitchClientId { get; set; } - - [Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")] - public string TwitchClientSecret { get; set; } - - [Comment(""" - Command and args which will be used to restart the bot. - Only used if bot is executed directly (NOT through the coordinator) - placeholders: - {0} -> shard id - {1} -> total shards - Linux default - cmd: dotnet - args: "Ellie.dll -- {0}" - Windows default - cmd: Ellie.exe - args: "{0}" - """)] - public RestartConfig RestartCommand { get; set; } - - public Creds() - { - Version = 7; - Token = string.Empty; - UsePrivilegedIntents = true; - OwnerIds = new List(); - TotalShards = 1; - GoogleApiKey = string.Empty; - Votes = new VotesSettings(string.Empty, string.Empty, string.Empty, string.Empty); - Patreon = new PatreonSettings(string.Empty, string.Empty, string.Empty, string.Empty); - BotListToken = string.Empty; - CleverbotApiKey = string.Empty; - Gpt3ApiKey = string.Empty; - BotCache = BotCacheImplemenation.Memory; - RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password="; - Db = new DbOptions() - { - Type = "sqlite", - ConnectionString = "Data Source=data/Ellie.db" - }; - - CoordinatorUrl = "http://localhost:3442"; - - RestartCommand = new RestartConfig(); - Google = new GoogleApiConfig(); - } - - public class DbOptions - : IDbOptions - { - [Comment(""" - Database type. "sqlite", "mysql" and "postgresql" are supported. - Default is "sqlite" - """)] - public string Type { get; set; } - - [Comment(""" - Database connection string. - You MUST change this if you're not using "sqlite" type. - Default is "Data Source=data/Ellie.db" - Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie" - Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;" - """)] - public string ConnectionString { get; set; } - } - - public sealed record PatreonSettings : IPatreonSettings - { - public string ClientId { get; set; } - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string ClientSecret { get; set; } - - [Comment( - """Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)""")] - public string CampaignId { get; set; } - - public PatreonSettings( - string accessToken, - string refreshToken, - string clientSecret, - string campaignId) - { - AccessToken = accessToken; - RefreshToken = refreshToken; - ClientSecret = clientSecret; - CampaignId = campaignId; - } - - public PatreonSettings() - { - } - } - - public sealed record VotesSettings : IVotesSettings - { - [Comment(""" - top.gg votes service url - This is the url of your instance of the Ellie.Votes api - Example: https://votes.my.cool.bot.com - """)] - public string TopggServiceUrl { get; set; } - - [Comment(""" - Authorization header value sent to the TopGG service url with each request - This should be equivalent to the TopggKey in your Ellie.Votes api appsettings.json file - """)] - public string TopggKey { get; set; } - - [Comment(""" - discords.com votes service url - This is the url of your instance of the Ellie.Votes api - Example: https://votes.my.cool.bot.com - """)] - public string DiscordsServiceUrl { get; set; } - - [Comment(""" - Authorization header value sent to the Discords service url with each request - This should be equivalent to the DiscordsKey in your Ellie.Votes api appsettings.json file - """)] - public string DiscordsKey { get; set; } - - public VotesSettings() - { - } - - public VotesSettings( - string topggServiceUrl, - string topggKey, - string discordsServiceUrl, - string discordsKey) - { - TopggServiceUrl = topggServiceUrl; - TopggKey = topggKey; - DiscordsServiceUrl = discordsServiceUrl; - DiscordsKey = discordsKey; - } - } -} - -public class GoogleApiConfig : IGoogleApiConfig -{ - public string SearchId { get; init; } - public string ImageSearchId { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Currency/CurrencyType.cs b/src/Ellie.Bot.Common/Currency/CurrencyType.cs deleted file mode 100644 index a3b6fcf..0000000 --- a/src/Ellie.Bot.Common/Currency/CurrencyType.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ellie.Services.Currency; - -public enum CurrencyType{ - Default -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Currency/IBankService.cs b/src/Ellie.Bot.Common/Currency/IBankService.cs deleted file mode 100644 index caca06d..0000000 --- a/src/Ellie.Bot.Common/Currency/IBankService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ellie.Modules.Gambling.Bank; - -public interface IBankService -{ - Task DepositAsync(ulong userId, long amount); - Task WithdrawAsync(ulong userId, long amount); - Task GetBalanceAsync(ulong userId); - Task AwardAsync(ulong userId, long amount); - Task TakeAsync(ulong userId, long amount); -} diff --git a/src/Ellie.Bot.Common/Currency/ICurrencyService.cs b/src/Ellie.Bot.Common/Currency/ICurrencyService.cs deleted file mode 100644 index 332e890..0000000 --- a/src/Ellie.Bot.Common/Currency/ICurrencyService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Ellie.Services.Currency; - -namespace Ellie.Services; - -public interface ICurrencyService -{ - Task GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default); - - Task AddBulkAsync( - IReadOnlyCollection userIds, - long amount, - TxData? txData, - CurrencyType type = CurrencyType.Default); - - Task RemoveBulkAsync( - IReadOnlyCollection userIds, - long amount, - TxData? txData, - CurrencyType type = CurrencyType.Default); - - Task AddAsync( - ulong userId, - long amount, - TxData? txData); - - Task AddAsync( - IUser user, - long amount, - TxData? txData); - - Task RemoveAsync( - ulong userId, - long amount, - TxData? txData); - - Task RemoveAsync( - IUser user, - long amount, - TxData? txData); -} diff --git a/src/Ellie.Bot.Common/Currency/ITxTracker.cs b/src/Ellie.Bot.Common/Currency/ITxTracker.cs deleted file mode 100644 index c0d3f7a..0000000 --- a/src/Ellie.Bot.Common/Currency/ITxTracker.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ellie.Services.Currency; - -namespace Ellie.Services; - -public interface ITxTracker -{ - Task TrackAdd(long amount, TxData? txData); - Task TrackRemove(long amount, TxData? txData); -} diff --git a/src/Ellie.Bot.Common/Currency/IWallet.cs b/src/Ellie.Bot.Common/Currency/IWallet.cs deleted file mode 100644 index 332c8d2..0000000 --- a/src/Ellie.Bot.Common/Currency/IWallet.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Ellie.Services.Currency; - -public interface IWallet -{ - public ulong UserId { get; } - - public Task GetBalance(); - public Task Take(long amount, TxData? txData); - public Task Add(long amount, TxData? txData); - - public async Task Transfer( - long amount, - IWallet to, - TxData? txData) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0."); - - if (txData is not null) - txData = txData with - { - OtherId = to.UserId - }; - - var succ = await Take(amount, txData); - - if (!succ) - return false; - - if (txData is not null) - txData = txData with - { - OtherId = UserId - }; - - await to.Add(amount, txData); - - return true; - } -} diff --git a/src/Ellie.Bot.Common/Currency/TxData.cs b/src/Ellie.Bot.Common/Currency/TxData.cs deleted file mode 100644 index 55bc40b..0000000 --- a/src/Ellie.Bot.Common/Currency/TxData.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Services.Currency; - -public record class TxData( - string Type, - string Extra, - string? Note = "", - ulong? OtherId = null); diff --git a/src/Ellie.Bot.Common/DbService.cs b/src/Ellie.Bot.Common/DbService.cs deleted file mode 100644 index 0355dfd..0000000 --- a/src/Ellie.Bot.Common/DbService.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable disable -using LinqToDB.Common; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database; - -namespace Ellie.Services; - -public abstract class DbService -{ - /// - /// Call this to apply all migrations - /// - public abstract Task SetupAsync(); - - public abstract DbContext CreateRawDbContext(string dbType, string connString); - public abstract DbContext GetDbContext(); -} diff --git a/src/Ellie.Bot.Common/DoAsUserMessage.cs b/src/Ellie.Bot.Common/DoAsUserMessage.cs deleted file mode 100644 index 0d86327..0000000 --- a/src/Ellie.Bot.Common/DoAsUserMessage.cs +++ /dev/null @@ -1,141 +0,0 @@ -using MessageType = Discord.MessageType; - -namespace Ellie.Modules.Administration; - -public sealed class DoAsUserMessage : IUserMessage -{ - private readonly string _message; - private IUserMessage _msg; - private readonly IUser _user; - - public DoAsUserMessage(SocketUserMessage msg, IUser user, string message) - { - _msg = msg; - _user = user; - _message = message; - } - - public ulong Id => _msg.Id; - - public DateTimeOffset CreatedAt => _msg.CreatedAt; - - public Task DeleteAsync(RequestOptions? options = null) - { - return _msg.DeleteAsync(options); - } - - public Task AddReactionAsync(IEmote emote, RequestOptions? options = null) - { - return _msg.AddReactionAsync(emote, options); - } - - public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null) - { - return _msg.RemoveReactionAsync(emote, user, options); - } - - public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null) - { - return _msg.RemoveReactionAsync(emote, userId, options); - } - - public Task RemoveAllReactionsAsync(RequestOptions? options = null) - { - return _msg.RemoveAllReactionsAsync(options); - } - - public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions? options = null) - { - return _msg.RemoveAllReactionsForEmoteAsync(emote, options); - } - - public IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, - RequestOptions? options = null) - { - return _msg.GetReactionUsersAsync(emoji, limit, options); - } - - public MessageType Type => _msg.Type; - - public MessageSource Source => _msg.Source; - - public bool IsTTS => _msg.IsTTS; - - public bool IsPinned => _msg.IsPinned; - - public bool IsSuppressed => _msg.IsSuppressed; - - public bool MentionedEveryone => _msg.MentionedEveryone; - - public string Content => _message; - - public string CleanContent => _msg.CleanContent; - - public DateTimeOffset Timestamp => _msg.Timestamp; - - public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp; - - public IMessageChannel Channel => _msg.Channel; - - public IUser Author => _user; - - public IThreadChannel Thread => _msg.Thread; - - public IReadOnlyCollection Attachments => _msg.Attachments; - - public IReadOnlyCollection Embeds => _msg.Embeds; - - public IReadOnlyCollection Tags => _msg.Tags; - - public IReadOnlyCollection MentionedChannelIds => _msg.MentionedChannelIds; - - public IReadOnlyCollection MentionedRoleIds => _msg.MentionedRoleIds; - - public IReadOnlyCollection MentionedUserIds => _msg.MentionedUserIds; - - public MessageActivity Activity => _msg.Activity; - - public MessageApplication Application => _msg.Application; - - public MessageReference Reference => _msg.Reference; - - public IReadOnlyDictionary Reactions => _msg.Reactions; - - public IReadOnlyCollection Components => _msg.Components; - - public IReadOnlyCollection Stickers => _msg.Stickers; - - public MessageFlags? Flags => _msg.Flags; - - public IMessageInteraction Interaction => _msg.Interaction; - public MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData; - - public Task ModifyAsync(Action func, RequestOptions? options = null) - { - return _msg.ModifyAsync(func, options); - } - - public Task PinAsync(RequestOptions? options = null) - { - return _msg.PinAsync(options); - } - - public Task UnpinAsync(RequestOptions? options = null) - { - return _msg.UnpinAsync(options); - } - - public Task CrosspostAsync(RequestOptions? options = null) - { - return _msg.CrosspostAsync(options); - } - - public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, - TagHandling roleHandling = TagHandling.Name, - TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - { - return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - } - - public IUserMessage ReferencedMessage => _msg.ReferencedMessage; -} diff --git a/src/Ellie.Bot.Common/Ellie.Bot.Common.csproj b/src/Ellie.Bot.Common/Ellie.Bot.Common.csproj index 589d454..fed3f63 100644 --- a/src/Ellie.Bot.Common/Ellie.Bot.Common.csproj +++ b/src/Ellie.Bot.Common/Ellie.Bot.Common.csproj @@ -34,7 +34,8 @@ - + + responses.en-US.json + - diff --git a/src/Ellie.Bot.Common/EllieModule.cs b/src/Ellie.Bot.Common/EllieModule.cs deleted file mode 100644 index 1a5ce63..0000000 --- a/src/Ellie.Bot.Common/EllieModule.cs +++ /dev/null @@ -1,143 +0,0 @@ -#nullable disable -using System.Globalization; - -// ReSharper disable InconsistentNaming - -namespace Ellie.Common; - -[UsedImplicitly(ImplicitUseTargetFlags.Default - | ImplicitUseTargetFlags.WithInheritors - | ImplicitUseTargetFlags.WithMembers)] -public abstract class EllieModule : ModuleBase -{ - protected CultureInfo Culture { get; set; } - - // Injected by Discord.net - public IBotStrings Strings { get; set; } - public ICommandHandler _cmdHandler { get; set; } - public ILocalization _localization { get; set; } - public IEmbedBuilderService _eb { get; set; } - public IEllieInteractionService _inter { get; set; } - - protected string prefix - => _cmdHandler.GetPrefix(ctx.Guild); - - protected ICommandContext ctx - => Context; - - protected override void BeforeExecute(CommandInfo command) - => Culture = _localization.GetCultureInfo(ctx.Guild?.Id); - - protected string GetText(in LocStr data) - => Strings.GetText(data, Culture); - - public Task SendErrorAsync( - string title, - string error, - string url = null, - string footer = null, - EllieInteraction inter = null) - => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer); - - public Task SendConfirmAsync( - string title, - string text, - string url = null, - string footer = null) - => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer); - - // - public Task SendErrorAsync(string text, EllieInteraction inter = null) - => ctx.Channel.SendAsync(_eb, text, MsgType.Error, inter); - - public Task SendConfirmAsync(string text, EllieInteraction inter = null) - => ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter); - - public Task SendPendingAsync(string text, EllieInteraction inter = null) - => ctx.Channel.SendAsync(_eb, text, MsgType.Pending, inter); - - - // localized normal - public Task ErrorLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendErrorAsync(GetText(str), inter); - - public Task PendingLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendPendingAsync(GetText(str), inter); - - public Task ConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendConfirmAsync(GetText(str), inter); - - // localized replies - public Task ReplyErrorLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter); - - public Task ReplyPendingLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter); - - public Task ReplyConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null) - => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter); - - public async Task PromptUserConfirmAsync(IEmbedBuilder embed) - { - embed.WithPendingColor().WithFooter("yes/no"); - - var msg = await ctx.Channel.EmbedAsync(embed); - try - { - var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id); - input = input?.ToUpperInvariant(); - - if (input != "YES" && input != "Y") - return false; - - return true; - } - finally - { - _ = Task.Run(() => msg.DeleteAsync()); - } - } - - // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ? - public async Task GetUserInputAsync(ulong userId, ulong channelId) - { - var userInputTask = new TaskCompletionSource(); - var dsc = (DiscordSocketClient)ctx.Client; - try - { - dsc.MessageReceived += MessageReceived; - - if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)) != userInputTask.Task) - return null; - - return await userInputTask.Task; - } - finally - { - dsc.MessageReceived -= MessageReceived; - } - - Task MessageReceived(SocketMessage arg) - { - _ = Task.Run(() => - { - if (arg is not SocketUserMessage userMsg - || userMsg.Channel is not ITextChannel - || userMsg.Author.Id != userId - || userMsg.Channel.Id != channelId) - return Task.CompletedTask; - - if (userInputTask.TrySetResult(arg.Content)) - userMsg.DeleteAfter(1); - - return Task.CompletedTask; - }); - return Task.CompletedTask; - } - } -} - -public abstract class EllieModule : EllieModule -{ - public TService _service { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/EllieTypeReader.cs b/src/Ellie.Bot.Common/EllieTypeReader.cs deleted file mode 100644 index 14af2c3..0000000 --- a/src/Ellie.Bot.Common/EllieTypeReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable - -namespace Ellie.Common.TypeReaders; - -[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)] -public abstract class EllieTypeReader : TypeReader -{ - public abstract ValueTask> ReadAsync(ICommandContext ctx, string input); - - public override async Task ReadAsync( - ICommandContext ctx, - string input, - IServiceProvider services) - => await ReadAsync(ctx, input); -} diff --git a/src/Ellie.Bot.Common/Extensions/DbExtensions.cs b/src/Ellie.Bot.Common/Extensions/DbExtensions.cs deleted file mode 100644 index c4b9b45..0000000 --- a/src/Ellie.Bot.Common/Extensions/DbExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Db.Models; -// todo fix these namespaces. It should only be Ellie.Bot.Db -using Ellie.Services.Database; - -namespace Ellie.Extensions; - -public static class DbExtensions -{ - public static DiscordUser GetOrCreateUser(this DbContext ctx, IUser original, Func, IQueryable>? includes = null) - => ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs b/src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs deleted file mode 100644 index 1db8ab0..0000000 --- a/src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs +++ /dev/null @@ -1,97 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Color = Discord.Color; - -namespace Ellie.Extensions; - -public static class ImagesharpExtensions -{ - // https://github.com/SixLabors/Samples/blob/master/ImageSharp/AvatarWithRoundedCorner/Program.cs - public static IImageProcessingContext ApplyRoundedCorners(this IImageProcessingContext ctx, float cornerRadius) - { - var size = ctx.GetCurrentSize(); - var corners = BuildCorners(size.Width, size.Height, cornerRadius); - - ctx.SetGraphicsOptions(new GraphicsOptions - { - Antialias = true, - // enforces that any part of this shape that has color is punched out of the background - AlphaCompositionMode = PixelAlphaCompositionMode.DestOut - }); - - foreach (var c in corners) - ctx = ctx.Fill(SixLabors.ImageSharp.Color.Red, c); - - return ctx; - } - - private static IPathCollection BuildCorners(int imageWidth, int imageHeight, float cornerRadius) - { - // first create a square - var rect = new RectangularPolygon(-0.5f, -0.5f, cornerRadius, cornerRadius); - - // then cut out of the square a circle so we are left with a corner - var cornerTopLeft = rect.Clip(new EllipsePolygon(cornerRadius - 0.5f, cornerRadius - 0.5f, cornerRadius)); - - // corner is now a corner shape positions top left - //lets make 3 more positioned correctly, we can do that by translating the original around the center of the image - - var rightPos = imageWidth - cornerTopLeft.Bounds.Width + 1; - var bottomPos = imageHeight - cornerTopLeft.Bounds.Height + 1; - - // move it across the width of the image - the width of the shape - var cornerTopRight = cornerTopLeft.RotateDegree(90).Translate(rightPos, 0); - var cornerBottomLeft = cornerTopLeft.RotateDegree(-90).Translate(0, bottomPos); - var cornerBottomRight = cornerTopLeft.RotateDegree(180).Translate(rightPos, bottomPos); - - return new PathCollection(cornerTopLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight); - } - - public static Color ToDiscordColor(this Rgba32 color) - => new(color.R, color.G, color.B); - - public static MemoryStream ToStream(this Image img, IImageFormat? format = null) - { - var imageStream = new MemoryStream(); - if (format?.Name == "GIF") - img.SaveAsGif(imageStream); - else - { - img.SaveAsPng(imageStream, - new() - { - ColorType = PngColorType.RgbWithAlpha, - CompressionLevel = PngCompressionLevel.DefaultCompression - }); - } - - 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/Ellie.Bot.Common/GlobalUsings.cs b/src/Ellie.Bot.Common/GlobalUsings.cs deleted file mode 100644 index 789a2ab..0000000 --- a/src/Ellie.Bot.Common/GlobalUsings.cs +++ /dev/null @@ -1,31 +0,0 @@ -// // global using System.Collections.Concurrent; -global using NonBlocking; -// -// // packages -global using Humanizer; -// -// // ellie -global using Ellie; -global using Ellie.Services; -global using Ellise.Common; // new project -global using Ellie.Common; // old + ellie specific things -global using Ellie.Common.Attributes; -global using Ellie.Extensions; - -// discord -global using Discord; -global using Discord.Commands; -global using Discord.Net; -global using Discord.WebSocket; - -// aliases -global using GuildPerm = Discord.GuildPermission; -global using ChannelPerm = Discord.ChannelPermission; -global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute; -global using LeftoverAttribute = Discord.Commands.RemainderAttribute; - -// non-essential -global using JetBrains.Annotations; - - -global using Serilog; \ No newline at end of file diff --git a/src/Ellie.Bot.Common/IBot.cs b/src/Ellie.Bot.Common/IBot.cs deleted file mode 100644 index 7473226..0000000 --- a/src/Ellie.Bot.Common/IBot.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie; - -public interface IBot -{ - IReadOnlyList GetCurrentGuildIds(); - event Func JoinedGuild; - IReadOnlyCollection AllGuildConfigs { get; } - bool IsReady { get; } -} diff --git a/src/Ellie.Bot.Common/ICloneable.cs b/src/Ellie.Bot.Common/ICloneable.cs deleted file mode 100644 index 89b3114..0000000 --- a/src/Ellie.Bot.Common/ICloneable.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public interface ICloneable - where T : new() -{ - public T Clone(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/ICurrencyProvider.cs b/src/Ellie.Bot.Common/ICurrencyProvider.cs deleted file mode 100644 index a6b1a2c..0000000 --- a/src/Ellie.Bot.Common/ICurrencyProvider.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Globalization; -using System.Numerics; - -namespace Ellie.Bot.Common; - -public interface ICurrencyProvider -{ - string GetCurrencySign(); -} - -public static class CurrencyHelper -{ - public static string N(T cur, IFormatProvider format) - where T : INumber - => cur.ToString("C0", format); - - public static string N(T cur, CultureInfo culture, string currencySign) - where T : INumber - => N(cur, GetCurrencyFormat(culture, currencySign)); - - private static IFormatProvider GetCurrencyFormat(CultureInfo culture, string currencySign) - { - var flowersCurrencyCulture = (CultureInfo)culture.Clone(); - flowersCurrencyCulture.NumberFormat.CurrencySymbol = currencySign; - flowersCurrencyCulture.NumberFormat.CurrencyNegativePattern = 5; - - return flowersCurrencyCulture; - } -} diff --git a/src/Ellie.Bot.Common/IDiscordPermOverrideService.cs b/src/Ellie.Bot.Common/IDiscordPermOverrideService.cs deleted file mode 100644 index 7b36d13..0000000 --- a/src/Ellie.Bot.Common/IDiscordPermOverrideService.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellise.Common; - -public interface IDiscordPermOverrideService -{ - bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm); -} diff --git a/src/Ellie.Bot.Common/IEllieCommandOptions.cs b/src/Ellie.Bot.Common/IEllieCommandOptions.cs deleted file mode 100644 index f3f60dd..0000000 --- a/src/Ellie.Bot.Common/IEllieCommandOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public interface IEllieCommandOptions -{ - void NormalizeOptions(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/ILogCommandService.cs b/src/Ellie.Bot.Common/ILogCommandService.cs deleted file mode 100644 index 127435f..0000000 --- a/src/Ellie.Bot.Common/ILogCommandService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Common; - -public interface ILogCommandService -{ - void AddDeleteIgnore(ulong xId); - Task LogServer(ulong guildId, ulong channelId, bool actionValue); - bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType); - LogSetting? GetGuildLogSettings(ulong guildId); - bool Log(ulong guildId, ulong? channelId, LogType type); -} - -public enum LogType -{ - Other, - MessageUpdated, - MessageDeleted, - UserJoined, - UserLeft, - UserBanned, - UserUnbanned, - UserUpdated, - ChannelCreated, - ChannelDestroyed, - ChannelUpdated, - UserPresence, - VoicePresence, - VoicePresenceTts, - UserMuted, - UserWarned, - - ThreadDeleted, - ThreadCreated -} diff --git a/src/Ellie.Bot.Common/IPermissionChecker.cs b/src/Ellie.Bot.Common/IPermissionChecker.cs deleted file mode 100644 index de9e14d..0000000 --- a/src/Ellie.Bot.Common/IPermissionChecker.cs +++ /dev/null @@ -1,13 +0,0 @@ -using OneOf; -using OneOf.Types; - -namespace Ellie.Bot.Common; - -public interface IPermissionChecker -{ - Task>> CheckAsync(IGuild guild, - IMessageChannel channel, - IUser author, - string module, - string? cmd); -} diff --git a/src/Ellie.Bot.Common/IPlaceholderProvider.cs b/src/Ellie.Bot.Common/IPlaceholderProvider.cs deleted file mode 100644 index c1a88c1..0000000 --- a/src/Ellie.Bot.Common/IPlaceholderProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public interface IPlaceholderProvider -{ - public IEnumerable<(string Name, Func Func)> GetPlaceholders(); -} diff --git a/src/Ellie.Bot.Common/Interaction/EllieInteraction.cs b/src/Ellie.Bot.Common/Interaction/EllieInteraction.cs deleted file mode 100644 index 5085253..0000000 --- a/src/Ellie.Bot.Common/Interaction/EllieInteraction.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Ellie; - -public sealed class EllieInteraction -{ - private readonly ulong _authorId; - private readonly ButtonBuilder _button; - private readonly Func _onClick; - private readonly bool _onlyAuthor; - public DiscordSocketClient Client { get; } - - private readonly TaskCompletionSource _interactionCompletedSource; - - private IUserMessage message = null!; - - public EllieInteraction(DiscordSocketClient client, - ulong authorId, - ButtonBuilder button, - Func onClick, - bool onlyAuthor) - { - _authorId = authorId; - _button = button; - _onClick = onClick; - _onlyAuthor = onlyAuthor; - _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - - Client = client; - } - - public async Task RunAsync(IUserMessage msg) - { - message = msg; - - Client.InteractionCreated += OnInteraction; - await Task.WhenAny(Task.Delay(15_000), _interactionCompletedSource.Task); - Client.InteractionCreated -= OnInteraction; - - await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build()); - } - - private Task OnInteraction(SocketInteraction arg) - { - if (arg is not SocketMessageComponent smc) - return Task.CompletedTask; - - if (smc.Message.Id != message.Id) - return Task.CompletedTask; - - if (_onlyAuthor && smc.User.Id != _authorId) - return Task.CompletedTask; - - if (smc.Data.CustomId != _button.CustomId) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - await ExecuteOnActionAsync(smc); - - // this should only be a thing on single-response buttons - _interactionCompletedSource.TrySetResult(true); - - if (!smc.HasResponded) - { - await smc.DeferAsync(); - } - }); - - return Task.CompletedTask; - } - - - public MessageComponent CreateComponent() - { - var comp = new ComponentBuilder() - .WithButton(_button); - - return comp.Build(); - } - - public Task ExecuteOnActionAsync(SocketMessageComponent smc) - => _onClick(smc); -} diff --git a/src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs b/src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs deleted file mode 100644 index 51c4bd1..0000000 --- a/src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie; - -/// -/// Represents essential interacation data -/// -/// Emote which will show on a button -/// Custom interaction id -public record EllieInteractionData(IEmote Emote, string CustomId, string? Text = null); diff --git a/src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs b/src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs deleted file mode 100644 index 2030b0d..0000000 --- a/src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Ellie; - -public class EllieInteractionService : IEllieInteractionService, IEService -{ - private readonly DiscordSocketClient _client; - - public EllieInteractionService(DiscordSocketClient client) - { - _client = client; - } - - public EllieInteraction Create( - ulong userId, - SimpleInteraction inter) - => new EllieInteraction(_client, - userId, - inter.Button, - inter.TriggerAsync, - onlyAuthor: true); -} diff --git a/src/Ellie.Bot.Common/Interaction/IEllieInteractionService.cs b/src/Ellie.Bot.Common/Interaction/IEllieInteractionService.cs deleted file mode 100644 index 7ed3878..0000000 --- a/src/Ellie.Bot.Common/Interaction/IEllieInteractionService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie; - -public interface IEllieInteractionService -{ - public EllieInteraction Create( - ulong userId, - SimpleInteraction inter); -} diff --git a/src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs b/src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs deleted file mode 100644 index e2c66ea..0000000 --- a/src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Ellie; - -public class SimpleInteraction -{ - public ButtonBuilder Button { get; } - private readonly Func _onClick; - private readonly T? _state; - - public SimpleInteraction(ButtonBuilder button, Func onClick, T? state = default) - { - Button = button; - _onClick = onClick; - _state = state; - } - - public async Task TriggerAsync(SocketMessageComponent smc) - { - await _onClick(smc, _state!); - } -} diff --git a/src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs b/src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs deleted file mode 100644 index b34bf46..0000000 --- a/src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Globalization; - -namespace Ellie.Marmalade; - -public interface IMarmaladeLoaderSevice -{ - Task LoadMarmaladeAsync(string marmaladeName); - Task UnloadMarmaladeAsync(string marmaladeName); - string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture); - string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture); - Task ReloadStrings(); - IReadOnlyCollection GetAllMarmalades(); - IReadOnlyCollection GetLoadedMarmalades(CultureInfo? cultureInfo = null); -} - -public sealed record MarmaladeStats(string Name, - string? Description, - IReadOnlyCollection Canaries); - -public sealed record CanaryStats(string Name, - string? Prefix, - IReadOnlyCollection Commands); - -public sealed record CanaryCommandStats(string Name); \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs b/src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs deleted file mode 100644 index 8ae4850..0000000 --- a/src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ellie.Marmalade; - -public enum MarmaladeLoadResult -{ - Success, - NotFound, - AlreadyLoaded, - Empty, - UnknownError, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs b/src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs deleted file mode 100644 index bb43ffb..0000000 --- a/src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ellie.Marmalade; - -public enum MarmaladeUnloadResult -{ - Success, - NotLoaded, - PossiblyUnable, - NotFound, -} diff --git a/src/Ellie.Bot.Common/MessageType.cs b/src/Ellie.Bot.Common/MessageType.cs deleted file mode 100644 index 0eea29a..0000000 --- a/src/Ellie.Bot.Common/MessageType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie.Common; - -public enum MsgType -{ - Ok, - Pending, - Error -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs deleted file mode 100644 index 1912c54..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -public interface IBehavior -{ - public virtual string Name => this.GetType().Name; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs deleted file mode 100644 index d8f5bc7..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// Executed if no command was found for this message -/// -public interface IExecNoCommand : IBehavior -{ - /// - /// Executed at the end of the lifecycle if no command was found - /// → - /// → - /// → - /// [ | **] - /// - /// - /// - /// A task representing completion - Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg); -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs deleted file mode 100644 index 1802d56..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// Implemented by modules to handle non-bot messages received -/// -public interface IExecOnMessage : IBehavior -{ - int Priority { get; } - - /// - /// Ran after a non-bot message was received - /// ** → - /// → - /// → - /// [ | ] - /// - /// Guild where the message was sent - /// The message that was received - /// Whether further processing of this message should be blocked - Task ExecOnMessageAsync(IGuild guild, IUserMessage msg); -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs deleted file mode 100644 index 3941a6c..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// This interface's method is executed after the command successfully finished execution. -/// ***There is no support for this method in Ellie services.*** -/// It is only meant to be used in medusa system -/// -public interface IExecPostCommand : IBehavior -{ - /// - /// Executed after a command was successfully executed - /// → - /// → - /// → - /// [** | ] - /// - /// Command context - /// Module name - /// Command name - /// A task representing completion - ValueTask ExecPostCommandAsync(ICommandContext ctx, string moduleName, string commandName); -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs deleted file mode 100644 index dc3139c..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// This interface's method is executed after a command was found but before it was executed. -/// Able to block further processing of a command -/// -public interface IExecPreCommand : IBehavior -{ - public int Priority { get; } - - /// - /// - /// Ran after a command was found but before execution. - /// - /// → - /// → - /// ** → - /// [ | ] - /// - /// Command context - /// Name of the module - /// Command info - /// Whether further processing of the command is blocked - Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command); -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs deleted file mode 100644 index 0b3df45..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// Implemented by services which may transform input before a command is searched for -/// -public interface IInputTransformer : IBehavior -{ - /// - /// Ran after a non-bot message was received - /// -> - /// ** -> - /// -> - /// [ OR ] - /// - /// Guild - /// Channel in which the message was sent - /// User who sent the message - /// Content of the message - /// New input, if any, otherwise null - Task TransformInput( - IGuild guild, - IMessageChannel channel, - IUser user, - string input); -} diff --git a/src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs b/src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs deleted file mode 100644 index ba5717c..0000000 --- a/src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ellie.Common.ModuleBehaviors; - -/// -/// All services which need to execute something after -/// the bot is ready should implement this interface -/// -public interface IReadyExecutor : IBehavior -{ - /// - /// Executed when bot is ready - /// - public Task OnReadyAsync(); -} diff --git a/src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs b/src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs deleted file mode 100644 index d813ef6..0000000 --- a/src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Modules.Patronage; - -public readonly struct FeatureLimitKey -{ - public string PrettyName { get; init; } - public string Key { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs b/src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs deleted file mode 100644 index 6cfcf54..0000000 --- a/src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie.Modules.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/Ellie.Bot.Common/Patronage/IPatronData.cs b/src/Ellie.Bot.Common/Patronage/IPatronData.cs deleted file mode 100644 index 66812ba..0000000 --- a/src/Ellie.Bot.Common/Patronage/IPatronData.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ellie.Modules.Patronage; - -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/Ellie.Bot.Common/Patronage/IPatronageService.cs b/src/Ellie.Bot.Common/Patronage/IPatronageService.cs deleted file mode 100644 index b6e7f01..0000000 --- a/src/Ellie.Bot.Common/Patronage/IPatronageService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Ellie.Db.Models; -using OneOf; - -namespace Ellie.Modules.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/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs b/src/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs deleted file mode 100644 index 83ee2ba..0000000 --- a/src/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Patronage; - -/// -/// 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/Ellie.Bot.Common/Patronage/Patron.cs b/src/Ellie.Bot.Common/Patronage/Patron.cs deleted file mode 100644 index d8d5713..0000000 --- a/src/Ellie.Bot.Common/Patronage/Patron.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Ellie.Modules.Patronage; - -public readonly struct Patron -{ - /// - /// Unique id assigned to this patron by the payment platform - /// - public string UniquePlatformUserId { 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/Ellie.Bot.Common/Patronage/PatronConfigData.cs b/src/Ellie.Bot.Common/Patronage/PatronConfigData.cs deleted file mode 100644 index c0a14b5..0000000 --- a/src/Ellie.Bot.Common/Patronage/PatronConfigData.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Ellie.Common.Yml; -using Cloneable; - -namespace Ellie.Modules.Patronage; - -[Cloneable] -public partial class PatronConfigData : ICloneable -{ - [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 2; - - [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/Ellie.Bot.Common/Patronage/PatronExtensions.cs b/src/Ellie.Bot.Common/Patronage/PatronExtensions.cs deleted file mode 100644 index 93ba48e..0000000 --- a/src/Ellie.Bot.Common/Patronage/PatronExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Ellie.Modules.Patronage; - -public static class PatronExtensions -{ - 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/Ellie.Bot.Common/Patronage/PatronTier.cs b/src/Ellie.Bot.Common/Patronage/PatronTier.cs deleted file mode 100644 index e0cab8a..0000000 --- a/src/Ellie.Bot.Common/Patronage/PatronTier.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ReSharper disable InconsistentNaming -namespace Ellie.Modules.Patronage; - -public enum PatronTier -{ - None, - I, - V, - X, - XX, - L, - C, - ComingSoon -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Patronage/QuotaLimit.cs b/src/Ellie.Bot.Common/Patronage/QuotaLimit.cs deleted file mode 100644 index b8f600c..0000000 --- a/src/Ellie.Bot.Common/Patronage/QuotaLimit.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Ellie.Db.Models; - -namespace Ellie.Modules.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/Ellie.Bot.Common/Patronage/QuotaPer.cs b/src/Ellie.Bot.Common/Patronage/QuotaPer.cs deleted file mode 100644 index 1849dd8..0000000 --- a/src/Ellie.Bot.Common/Patronage/QuotaPer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie.Modules.Patronage; - -public enum QuotaPer -{ - PerHour, - PerDay, - PerMonth, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Patronage/SubscriptionChargeStatus.cs b/src/Ellie.Bot.Common/Patronage/SubscriptionChargeStatus.cs deleted file mode 100644 index acd2852..0000000 --- a/src/Ellie.Bot.Common/Patronage/SubscriptionChargeStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Patronage; - -public enum SubscriptionChargeStatus -{ - Paid, - Refunded, - Unpaid, - Other, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Patronage/UserQuotaStats.cs b/src/Ellie.Bot.Common/Patronage/UserQuotaStats.cs deleted file mode 100644 index 13028cc..0000000 --- a/src/Ellie.Bot.Common/Patronage/UserQuotaStats.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ellie.Modules.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/Ellie.Bot.Common/Replacements/ReplacementBuilder.cs b/src/Ellie.Bot.Common/Replacements/ReplacementBuilder.cs deleted file mode 100644 index b842e63..0000000 --- a/src/Ellie.Bot.Common/Replacements/ReplacementBuilder.cs +++ /dev/null @@ -1,164 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace Ellie.Common; - -public class ReplacementBuilder -{ - private static readonly Regex _rngRegex = new("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", - RegexOptions.Compiled); - - private readonly ConcurrentDictionary> _regex = new(); - - private readonly ConcurrentDictionary> _reps = new(); - - public ReplacementBuilder() - => WithRngRegex(); - - public ReplacementBuilder WithDefault( - IUser usr, - IMessageChannel ch, - SocketGuild g, - DiscordSocketClient client) - => WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client); - - public ReplacementBuilder WithDefault(ICommandContext ctx) - => WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client); - - public ReplacementBuilder WithMention(DiscordSocketClient client) - { - _reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention); - return this; - } - - public ReplacementBuilder WithClient(DiscordSocketClient client) - { - WithMention(client); - - _reps.TryAdd("%bot.status%", () => client.Status.ToString()); - _reps.TryAdd("%bot.latency%", () => client.Latency.ToString()); - _reps.TryAdd("%bot.name%", () => client.CurrentUser.Username); - _reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString()); - _reps.TryAdd("%bot.time%", - () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); - _reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator); - _reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString()); - _reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl().ToString()); - - WithStats(client); - return this; - } - - public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g) - { - _reps.TryAdd("%server%", () => g is null ? "DM" : g.Name); - _reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString()); - _reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name); - _reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl); - _reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?"); - _reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString()); - _reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString()); - // todo fix - // _reps.TryAdd("%server.time%", - // () => - // { - // var to = TimeZoneInfo.Local; - // if (g is not null) - // { - // if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) - // to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; - // } - // - // return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ") - // + to.StandardName.GetInitials(); - // }); - return this; - } - - public ReplacementBuilder WithChannel(IMessageChannel ch) - { - _reps.TryAdd("%channel%", () => ch.Name); - _reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); - _reps.TryAdd("%channel.name%", () => ch.Name); - _reps.TryAdd("%channel.id%", () => ch.Id.ToString()); - _reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy")); - _reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-"); - _reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-"); - return this; - } - - public ReplacementBuilder WithUser(IUser user) - { - WithManyUsers(new[] { user }); - return this; - } - - public ReplacementBuilder WithManyUsers(IEnumerable users) - { - _reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention))); - _reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention))); - _reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString()))); - _reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username))); - _reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator))); - _reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString()))); - _reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); - _reps.TryAdd("%user.created_time%", - () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm")))); - _reps.TryAdd("%user.created_date%", - () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy")))); - _reps.TryAdd("%user.joined_time%", - () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"))); - _reps.TryAdd("%user.joined_date%", - () => string.Join(" ", - users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"))); - return this; - } - - private ReplacementBuilder WithStats(DiscordSocketClient c) - { - _reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString()); - _reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString()); - _reps.TryAdd("%shard.id%", () => c.ShardId.ToString()); - return this; - } - - public ReplacementBuilder WithRngRegex() - { - var rng = new EllieRandom(); - _regex.TryAdd(_rngRegex, - match => - { - if (!int.TryParse(match.Groups["from"].ToString(), out var from)) - from = 0; - if (!int.TryParse(match.Groups["to"].ToString(), out var to)) - to = 0; - - if (from == 0 && to == 0) - return rng.Next(0, 11).ToString(); - - if (from >= to) - return string.Empty; - - return rng.Next(from, to + 1).ToString(); - }); - return this; - } - - public ReplacementBuilder WithOverride(string key, Func output) - { - _reps.AddOrUpdate(key, output, delegate { return output; }); - return this; - } - - public Replacer Build() - => new(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray()); - - public ReplacementBuilder WithProviders(IEnumerable phProviders) - { - foreach (var provider in phProviders) - foreach (var ovr in provider.GetPlaceholders()) - _reps.TryAdd(ovr.Name, ovr.Func); - - return this; - } -} diff --git a/src/Ellie.Bot.Common/Replacements/Replacer.cs b/src/Ellie.Bot.Common/Replacements/Replacer.cs deleted file mode 100644 index aaee24f..0000000 --- a/src/Ellie.Bot.Common/Replacements/Replacer.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace Ellie.Common; - -public class Replacer -{ - private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; - private readonly IEnumerable<(string Key, Func Text)> _replacements; - - public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> regex) - { - _replacements = replacements; - _regex = regex; - } - - public string Replace(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return input; - - foreach (var (key, text) in _replacements) - { - if (input.Contains(key)) - input = input.Replace(key, text(), StringComparison.InvariantCulture); - } - - foreach (var item in _regex) - input = item.Regex.Replace(input, m => item.Replacement(m)); - - return input; - } - - public SmartText Replace(SmartText data) - => data switch - { - SmartEmbedText embedData => Replace(embedData) with - { - PlainText = Replace(embedData.PlainText), - Color = embedData.Color - }, - SmartPlainText plain => Replace(plain), - SmartEmbedTextArray arr => Replace(arr), - _ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type") - }; - - private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr) - => new() - { - Embeds = embedArr.Embeds.Map(e => Replace(e) with - { - Color = e.Color - }), - Content = Replace(embedArr.Content) - }; - - private SmartPlainText Replace(SmartPlainText plain) - => Replace(plain.Text); - - private T Replace(T embedData) where T : SmartEmbedTextBase, new() - { - var newEmbedData = new T - { - Description = Replace(embedData.Description), - Title = Replace(embedData.Title), - Thumbnail = Replace(embedData.Thumbnail), - Image = Replace(embedData.Image), - Url = Replace(embedData.Url), - Author = embedData.Author is null - ? null - : new() - { - Name = Replace(embedData.Author.Name), - IconUrl = Replace(embedData.Author.IconUrl) - }, - Fields = embedData.Fields?.Map(f => new SmartTextEmbedField - { - Name = Replace(f.Name), - Value = Replace(f.Value), - Inline = f.Inline - }), - Footer = embedData.Footer is null - ? null - : new() - { - Text = Replace(embedData.Footer.Text), - IconUrl = Replace(embedData.Footer.IconUrl) - } - }; - - return newEmbedData; - } -} diff --git a/src/Ellie.Bot.Common/Services/CommandHandler.cs b/src/Ellie.Bot.Common/Services/CommandHandler.cs deleted file mode 100644 index 5952e3d..0000000 --- a/src/Ellie.Bot.Common/Services/CommandHandler.cs +++ /dev/null @@ -1,427 +0,0 @@ -#nullable disable -using Ellie.Common.Configs; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using ExecuteResult = Discord.Commands.ExecuteResult; -using PreconditionResult = Discord.Commands.PreconditionResult; - -namespace Ellie.Services; - -public class CommandHandler : IEService, IReadyExecutor, ICommandHandler -{ - private const int GLOBAL_COMMANDS_COOLDOWN = 750; - - private const float ONE_THOUSANDTH = 1.0f / 1000; - - public event Func CommandExecuted = delegate { return Task.CompletedTask; }; - public event Func CommandErrored = delegate { return Task.CompletedTask; }; - - //userid/msg count - public ConcurrentDictionary UserMessagesSent { get; } = new(); - - public ConcurrentHashSet UsersOnShortCooldown { get; } = new(); - - private readonly DiscordSocketClient _client; - private readonly CommandService _commandService; - private readonly BotConfigService _bss; - private readonly IBot _bot; - private readonly IBehaviorHandler _behaviorHandler; - private readonly IServiceProvider _services; - - private readonly ConcurrentDictionary _prefixes; - - private readonly DbService _db; - // private readonly InteractionService _interactions; - - public CommandHandler( - DiscordSocketClient client, - DbService db, - CommandService commandService, - BotConfigService bss, - IBot bot, - IBehaviorHandler behaviorHandler, - // InteractionService interactions, - IServiceProvider services) - { - _client = client; - _commandService = commandService; - _bss = bss; - _bot = bot; - _behaviorHandler = behaviorHandler; - _db = db; - _services = services; - // _interactions = interactions; - - _prefixes = bot.AllGuildConfigs.Where(x => x.Prefix is not null) - .ToDictionary(x => x.GuildId, x => x.Prefix) - .ToConcurrent(); - - } - - public async Task OnReadyAsync() - { - Log.Information("Command handler runnning on ready"); - // clear users on short cooldown every GLOBAL_COMMANDS_COOLDOWN miliseconds - using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(GLOBAL_COMMANDS_COOLDOWN)); - while (await timer.WaitForNextTickAsync()) - UsersOnShortCooldown.Clear(); - } - - public string GetPrefix(IGuild guild) - => GetPrefix(guild?.Id); - - public string GetPrefix(ulong? id = null) - { - if (id is null || !_prefixes.TryGetValue(id.Value, out var prefix)) - return _bss.Data.Prefix; - - return prefix; - } - - public string SetDefaultPrefix(string prefix) - { - if (string.IsNullOrWhiteSpace(prefix)) - throw new ArgumentNullException(nameof(prefix)); - - _bss.ModifyConfig(bs => - { - bs.Prefix = prefix; - }); - - return prefix; - } - - public string SetPrefix(IGuild guild, string prefix) - { - if (string.IsNullOrWhiteSpace(prefix)) - throw new ArgumentNullException(nameof(prefix)); - if (guild is null) - throw new ArgumentNullException(nameof(guild)); - - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guild.Id, set => set); - gc.Prefix = prefix; - uow.SaveChanges(); - } - - _prefixes[guild.Id] = prefix; - - return prefix; - } - - public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText) - { - if (guildId is not null) - { - var guild = _client.GetGuild(guildId.Value); - if (guild?.GetChannel(channelId) is not SocketTextChannel channel) - { - Log.Warning("Channel for external execution not found"); - return; - } - - try - { - IUserMessage msg = await channel.SendMessageAsync(commandText); - msg = (IUserMessage)await channel.GetMessageAsync(msg.Id); - await TryRunCommand(guild, channel, msg); - //msg.DeleteAfter(5); - } - catch { } - } - } - - public Task StartHandling() - { - _client.MessageReceived += MessageReceivedHandler; - // _client.SlashCommandExecuted += SlashCommandExecuted; - return Task.CompletedTask; - } - - // private async Task SlashCommandExecuted(SocketSlashCommand arg) - // { - // var ctx = new SocketInteractionContext(_client, arg); - // await _interactions.ExecuteCommandAsync(ctx, _services); - // } - - private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints) - { - if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal) - { - Log.Information(""" - Command Executed after {ExecTime}s - User: {User} - Server: {Server} - Channel: {Channel} - Message: {Message} - """, - string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))), - usrMsg.Author + " [" + usrMsg.Author.Id + "]", - channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]", - channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]", - usrMsg.Content); - } - else - { - Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}", - channel?.Guild.Id.ToString() ?? "-", - channel?.Id.ToString() ?? "-", - usrMsg.Author.Id, - usrMsg.Content.TrimTo(10)); - } - - return Task.CompletedTask; - } - - private void LogErroredExecution( - string errorMessage, - IUserMessage usrMsg, - ITextChannel channel, - params int[] execPoints) - { - if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal) - { - Log.Warning(""" - Command Errored after {ExecTime}s - User: {User} - Server: {Guild} - Channel: {Channel} - Message: {Message} - Error: {ErrorMessage} - """, - string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))), - usrMsg.Author + " [" + usrMsg.Author.Id + "]", - channel is null ? "DM" : channel.Guild.Name + " [" + channel.Guild.Id + "]", - channel is null ? "DM" : channel.Name + " [" + channel.Id + "]", - usrMsg.Content, - errorMessage); - } - else - { - Log.Warning(""" - Err | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message} - Err: {ErrorMessage} - """, - channel?.Guild.Id.ToString() ?? "-", - channel?.Id.ToString() ?? "-", - usrMsg.Author.Id, - usrMsg.Content.TrimTo(10), - errorMessage); - } - } - - private Task MessageReceivedHandler(SocketMessage msg) - { - //no bots, wait until bot connected and initialized - if (msg.Author.IsBot || !_bot.IsReady) - return Task.CompletedTask; - - if (msg is not SocketUserMessage usrMsg) - return Task.CompletedTask; - - Task.Run(async () => - { - try - { -#if !GLOBAL_ELLIE - // track how many messages each user is sending - UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old); -#endif - - var channel = msg.Channel; - var guild = (msg.Channel as SocketTextChannel)?.Guild; - - await TryRunCommand(guild, channel, usrMsg); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in CommandHandler"); - if (ex.InnerException is not null) - Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler"); - } - }); - - return Task.CompletedTask; - } - - public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg) - { - var startTime = Environment.TickCount; - - var blocked = await _behaviorHandler.RunExecOnMessageAsync(guild, usrMsg); - if (blocked) - return; - - var blockTime = Environment.TickCount - startTime; - - var messageContent = await _behaviorHandler.RunInputTransformersAsync(guild, usrMsg); - - var prefix = GetPrefix(guild?.Id); - var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase); - // execute the command and measure the time it took - if (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture)) - { - var context = new CommandContext(_client, usrMsg); - var (success, error, info) = await ExecuteCommandAsync(context, - messageContent, - isPrefixCommand ? 1 : prefix.Length, - _services, - MultiMatchHandling.Best); - - startTime = Environment.TickCount - startTime; - - // if a command is found - if (info is not null) - { - // if it successfully executed - if (success) - { - await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime); - await CommandExecuted(usrMsg, info); - await _behaviorHandler.RunPostCommandAsync(context, info.Module.GetTopLevelModule().Name, info); - return; - } - - // if it errored - if (error is not null) - { - error = HumanizeError(error); - LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime); - - if (guild is not null) - await CommandErrored(info, channel as ITextChannel, error); - - return; - } - } - } - - await _behaviorHandler.RunOnNoCommandAsync(guild, usrMsg); - } - - private string HumanizeError(string error) - { - if (error.Contains("parse int", StringComparison.OrdinalIgnoreCase) - || error.Contains("parse float")) - return "Invalid number specified. Make sure you're specifying parameters in the correct order."; - - return error; - } - - public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync( - ICommandContext context, - string input, - int argPos, - IServiceProvider serviceProvider, - MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteCommand(context, input[argPos..], serviceProvider, multiMatchHandling); - - - public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand( - ICommandContext context, - string input, - IServiceProvider services, - MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - { - var searchResult = _commandService.Search(context, input); - if (!searchResult.IsSuccess) - return (false, null, null); - - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); - - foreach (var match in commands) - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services); - - var successfulPreconditions = preconditionResults.Where(x => x.Value.IsSuccess).ToArray(); - - if (successfulPreconditions.Length == 0) - { - //All preconditions failed, return the one from the highest priority command - var bestCandidate = preconditionResults.OrderByDescending(x => x.Key.Command.Priority) - .FirstOrDefault(x => !x.Value.IsSuccess); - return (false, bestCandidate.Value.ErrorReason, commands[0].Command); - } - - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) - { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services); - - if (parseResult.Error == CommandError.MultipleMatches) - { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) - { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues - .Map(x => x.Values.MaxBy(y => y.Score)); - paramList = parseResult.ParamValues - .Map(x => x.Values.MaxBy(y => y.Score)); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; - } - } - - parseResultsDict[pair.Key] = parseResult; - } - - // Calculates the 'score' of a command given a parse result - float CalculateScore(CommandMatch match, ParseResult parseResult) - { - float argValuesScore = 0, paramValuesScore = 0; - - if (match.Command.Parameters.Count > 0) - { - var argValuesSum = - parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) - ?? 0; - var paramValuesSum = - parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) - ?? 0; - - argValuesScore = argValuesSum / match.Command.Parameters.Count; - paramValuesScore = paramValuesSum / match.Command.Parameters.Count; - } - - var totalArgsScore = (argValuesScore + paramValuesScore) / 2; - return match.Command.Priority + (totalArgsScore * 0.99f); - } - - //Order the parse results by their score so that we choose the most likely result to execute - var parseResults = parseResultsDict.OrderByDescending(x => CalculateScore(x.Key, x.Value)).ToList(); - - var successfulParses = parseResults.Where(x => x.Value.IsSuccess).ToArray(); - - if (successfulParses.Length == 0) - { - //All parses failed, return the one from the highest priority command, using score as a tie breaker - var bestMatch = parseResults.FirstOrDefault(x => !x.Value.IsSuccess); - return (false, bestMatch.Value.ErrorReason, commands[0].Command); - } - - var cmd = successfulParses[0].Key.Command; - - // Bot will ignore commands which are ran more often than what specified by - // GlobalCommandsCooldown constant (miliseconds) - if (!UsersOnShortCooldown.Add(context.Message.Author.Id)) - return (false, null, cmd); - //return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown."); - - var blocked = await _behaviorHandler.RunPreCommandAsync(context, cmd); - if (blocked) - return (false, null, cmd); - - //If we get this far, at least one parse was successful. Execute the most likely overload. - var chosenOverload = successfulParses[0]; - var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services); - - if (execResult.Exception is not null - && (execResult.Exception is not HttpException he - || he.DiscordCode != DiscordErrorCode.InsufficientPermissions)) - Log.Warning(execResult.Exception, "Command Error"); - - return (true, null, cmd); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Currency/CurrencyService.cs b/src/Ellie.Bot.Common/Services/Currency/CurrencyService.cs deleted file mode 100644 index 873538e..0000000 --- a/src/Ellie.Bot.Common/Services/Currency/CurrencyService.cs +++ /dev/null @@ -1,109 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Db.Models; -using Ellie.Services.Currency; - -namespace Ellie.Services; - -public sealed class CurrencyService : ICurrencyService, IEService -{ - private readonly DbService _db; - private readonly ITxTracker _txTracker; - - public CurrencyService(DbService db, ITxTracker txTracker) - { - _db = db; - _txTracker = txTracker; - } - - public Task GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default) - { - if (type == CurrencyType.Default) - return Task.FromResult(new DefaultWallet(userId, _db)); - - throw new ArgumentOutOfRangeException(nameof(type)); - } - - public async Task AddBulkAsync( - IReadOnlyCollection userIds, - long amount, - TxData txData, - CurrencyType type = CurrencyType.Default) - { - if (type == CurrencyType.Default) - { - foreach (var userId in userIds) - { - var wallet = await GetWalletAsync(userId); - await wallet.Add(amount, txData); - } - - return; - } - - throw new ArgumentOutOfRangeException(nameof(type)); - } - - public async Task RemoveBulkAsync( - IReadOnlyCollection userIds, - long amount, - TxData txData, - CurrencyType type = CurrencyType.Default) - { - if (type == CurrencyType.Default) - { - await using var ctx = _db.GetDbContext(); - await ctx - .GetTable() - .Where(x => userIds.Contains(x.UserId)) - .UpdateAsync(du => new() - { - CurrencyAmount = du.CurrencyAmount >= amount - ? du.CurrencyAmount - amount - : 0 - }); - await ctx.SaveChangesAsync(); - return; - } - - throw new ArgumentOutOfRangeException(nameof(type)); - } - - public async Task AddAsync( - ulong userId, - long amount, - TxData txData) - { - var wallet = await GetWalletAsync(userId); - await wallet.Add(amount, txData); - await _txTracker.TrackAdd(amount, txData); - } - - public async Task AddAsync( - IUser user, - long amount, - TxData txData) - => await AddAsync(user.Id, amount, txData); - - public async Task RemoveAsync( - ulong userId, - long amount, - TxData txData) - { - if (amount == 0) - return true; - - var wallet = await GetWalletAsync(userId); - var result = await wallet.Take(amount, txData); - if(result) - await _txTracker.TrackRemove(amount, txData); - return result; - } - - public async Task RemoveAsync( - IUser user, - long amount, - TxData txData) - => await RemoveAsync(user.Id, amount, txData); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Currency/CurrencyServiceExtensions.cs b/src/Ellie.Bot.Common/Services/Currency/CurrencyServiceExtensions.cs deleted file mode 100644 index 874430c..0000000 --- a/src/Ellie.Bot.Common/Services/Currency/CurrencyServiceExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Ellie.Services.Currency; - -namespace Ellie.Services; - -public static class CurrencyServiceExtensions -{ - public static async Task GetBalanceAsync(this ICurrencyService cs, ulong userId) - { - var wallet = await cs.GetWalletAsync(userId); - return await wallet.GetBalance(); - } - - // FUTURE should be a transaction - public static async Task TransferAsync( - this ICurrencyService cs, - IEmbedBuilderService ebs, - IUser from, - IUser to, - long amount, - string? note, - string formattedAmount) - { - var fromWallet = await cs.GetWalletAsync(from.Id); - var toWallet = await cs.GetWalletAsync(to.Id); - - var extra = new TxData("gift", from.ToString()!, note, from.Id); - - if (await fromWallet.Transfer(amount, toWallet, extra)) - { - await to.SendConfirmAsync(ebs, - string.IsNullOrWhiteSpace(note) - ? $"Received {formattedAmount} from {from} " - : $"Received {formattedAmount} from {from}: {note}"); - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Currency/DefaultWallet.cs b/src/Ellie.Bot.Common/Services/Currency/DefaultWallet.cs deleted file mode 100644 index f39ce2c..0000000 --- a/src/Ellie.Bot.Common/Services/Currency/DefaultWallet.cs +++ /dev/null @@ -1,115 +0,0 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Db.Models; -using Ellie.Services.Database.Models; - -namespace Ellie.Services.Currency; - -public class DefaultWallet : IWallet -{ - private readonly DbService _db; - public ulong UserId { get; } - - public DefaultWallet(ulong userId, DbService db) - { - UserId = userId; - _db = db; - } - - public async Task GetBalance() - { - await using var ctx = _db.GetDbContext(); - var userId = UserId; - return await ctx - .GetTable() - .Where(x => x.UserId == userId) - .Select(x => x.CurrencyAmount) - .FirstOrDefaultAsync(); - } - - public async Task Take(long amount, TxData? txData) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount), "Amount to take must be non negative."); - - await using var ctx = _db.GetDbContext(); - - var userId = UserId; - var changed = await ctx - .GetTable() - .Where(x => x.UserId == userId && x.CurrencyAmount >= amount) - .UpdateAsync(x => new() - { - CurrencyAmount = x.CurrencyAmount - amount - }); - - if (changed == 0) - return false; - - if (txData is not null) - { - await ctx - .GetTable() - .InsertAsync(() => new() - { - Amount = -amount, - Note = txData.Note, - UserId = userId, - Type = txData.Type, - Extra = txData.Extra, - OtherId = txData.OtherId, - DateAdded = DateTime.UtcNow - }); - } - - return true; - } - - public async Task Add(long amount, TxData? txData) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0."); - - await using var ctx = _db.GetDbContext(); - var userId = UserId; - - await using (var tran = await ctx.Database.BeginTransactionAsync()) - { - var changed = await ctx - .GetTable() - .Where(x => x.UserId == userId) - .UpdateAsync(x => new() - { - CurrencyAmount = x.CurrencyAmount + amount - }); - - if (changed == 0) - { - await ctx - .GetTable() - .Value(x => x.UserId, userId) - .Value(x => x.Username, "Unknown") - .Value(x => x.Discriminator, "????") - .Value(x => x.CurrencyAmount, amount) - .InsertAsync(); - } - - await tran.CommitAsync(); - } - - if (txData is not null) - { - await ctx.GetTable() - .InsertAsync(() => new() - { - Amount = amount, - UserId = userId, - Note = txData.Note, - Type = txData.Type, - Extra = txData.Extra, - OtherId = txData.OtherId, - DateAdded = DateTime.UtcNow - }); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Currency/GamblingTxTracker.cs b/src/Ellie.Bot.Common/Services/Currency/GamblingTxTracker.cs deleted file mode 100644 index 9ad06f8..0000000 --- a/src/Ellie.Bot.Common/Services/Currency/GamblingTxTracker.cs +++ /dev/null @@ -1,110 +0,0 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Services.Currency; -using Ellie.Services.Database.Models; - -namespace Ellie.Services; - -public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor -{ - private static readonly IReadOnlySet _gamblingTypes = new HashSet(new[] - { - "lula", - "betroll", - "betflip", - "blackjack", - "betdraw", - "slot", - }); - - private ConcurrentDictionary _stats = new(); - - private readonly DbService _db; - - public GamblingTxTracker(DbService db) - { - _db = db; - } - - public async Task OnReadyAsync() - { - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - while (await timer.WaitForNextTickAsync()) - { - await using var ctx = _db.GetDbContext(); - await using var trans = await ctx.Database.BeginTransactionAsync(); - - try - { - var keys = _stats.Keys; - foreach (var key in keys) - { - if (_stats.TryRemove(key, out var stat)) - { - await ctx.GetTable() - .InsertOrUpdateAsync(() => new() - { - Feature = key, - Bet = stat.Bet, - PaidOut = stat.PaidOut, - DateAdded = DateTime.UtcNow - }, old => new() - { - Bet = old.Bet + stat.Bet, - PaidOut = old.PaidOut + stat.PaidOut, - }, () => new() - { - Feature = key - }); - } - } - } - catch (Exception ex) - { - Log.Error(ex, "An error occurred in gambling tx tracker"); - } - finally - { - await trans.CommitAsync(); - } - } - } - - public Task TrackAdd(long amount, TxData? txData) - { - if (txData is null) - return Task.CompletedTask; - - if (_gamblingTypes.Contains(txData.Type)) - { - _stats.AddOrUpdate(txData.Type, - _ => (0, amount), - (_, old) => (old.Bet, old.PaidOut + amount)); - } - - return Task.CompletedTask; - } - - public Task TrackRemove(long amount, TxData? txData) - { - if (txData is null) - return Task.CompletedTask; - - if (_gamblingTypes.Contains(txData.Type)) - { - _stats.AddOrUpdate(txData.Type, - _ => (amount, 0), - (_, old) => (old.Bet + amount, old.PaidOut)); - } - - return Task.CompletedTask; - } - - public async Task> GetAllAsync() - { - await using var ctx = _db.GetDbContext(); - return await ctx.Set() - .ToListAsyncEF(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IBehaviourExecutor.cs b/src/Ellie.Bot.Common/Services/IBehaviourExecutor.cs deleted file mode 100644 index 3788910..0000000 --- a/src/Ellie.Bot.Common/Services/IBehaviourExecutor.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public interface IBehaviorHandler -{ - Task AddAsync(ICustomBehavior behavior); - Task AddRangeAsync(IEnumerable behavior); - Task RemoveAsync(ICustomBehavior behavior); - Task RemoveRangeAsync(IEnumerable behs); - - Task RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg); - Task RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg); - Task RunPreCommandAsync(ICommandContext context, CommandInfo cmd); - ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd); - Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg); - void Initialize(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ICommandHandler.cs b/src/Ellie.Bot.Common/Services/ICommandHandler.cs deleted file mode 100644 index 976a38f..0000000 --- a/src/Ellie.Bot.Common/Services/ICommandHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ellie.Services; - -public interface ICommandHandler -{ - string GetPrefix(IGuild ctxGuild); - string GetPrefix(ulong? id = null); - string SetDefaultPrefix(string toSet); - string SetPrefix(IGuild ctxGuild, string toSet); - ConcurrentDictionary UserMessagesSent { get; } - - Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ICoordinator.cs b/src/Ellie.Bot.Common/Services/ICoordinator.cs deleted file mode 100644 index 62d8cd9..0000000 --- a/src/Ellie.Bot.Common/Services/ICoordinator.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public interface ICoordinator -{ - bool RestartBot(); - void Die(bool graceful); - bool RestartShard(int shardId); - IList GetAllShardStatuses(); - int GetGuildCount(); - Task Reload(); -} - -public class ShardStatus -{ - public ConnectionState ConnectionState { get; set; } - public DateTime LastUpdate { get; set; } - public int ShardId { get; set; } - public int GuildCount { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ICustomBehavior.cs b/src/Ellie.Bot.Common/Services/ICustomBehavior.cs deleted file mode 100644 index bf1a855..0000000 --- a/src/Ellie.Bot.Common/Services/ICustomBehavior.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ellie.Common.ModuleBehaviors; - -namespace Ellie.Services; - -public interface ICustomBehavior - : IExecOnMessage, - IInputTransformer, - IExecPreCommand, - IExecNoCommand, - IExecPostCommand -{ - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IEService.cs b/src/Ellie.Bot.Common/Services/IEService.cs deleted file mode 100644 index c915d94..0000000 --- a/src/Ellie.Bot.Common/Services/IEService.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -/// -/// All services must implement this interface in order to be auto-discovered by the DI system -/// -public interface IEService -{ -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IEmbedBuilderService.cs b/src/Ellie.Bot.Common/Services/IEmbedBuilderService.cs deleted file mode 100644 index 677660e..0000000 --- a/src/Ellie.Bot.Common/Services/IEmbedBuilderService.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable disable -using Ellie.Common.Configs; - -namespace Ellie.Services; - -public interface IEmbedBuilderService -{ - IEmbedBuilder Create(ICommandContext ctx = null); - IEmbedBuilder Create(EmbedBuilder eb); -} - -public class EmbedBuilderService : IEmbedBuilderService, IEService -{ - private readonly BotConfigService _botConfigService; - - public EmbedBuilderService(BotConfigService botConfigService) - => _botConfigService = botConfigService; - - public IEmbedBuilder Create(ICommandContext ctx = null) - => new DiscordEmbedBuilderWrapper(_botConfigService.Data); - - public IEmbedBuilder Create(EmbedBuilder embed) - => new DiscordEmbedBuilderWrapper(_botConfigService.Data, embed); -} - -public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder -{ - private readonly BotConfig _botConfig; - private EmbedBuilder embed; - - public DiscordEmbedBuilderWrapper(in BotConfig botConfig, EmbedBuilder embed = null) - { - _botConfig = botConfig; - this.embed = embed ?? new EmbedBuilder(); - } - - public IEmbedBuilder WithDescription(string desc) - => Wrap(embed.WithDescription(desc)); - - public IEmbedBuilder WithTitle(string title) - => Wrap(embed.WithTitle(title)); - - public IEmbedBuilder AddField(string title, object value, bool isInline = false) - => Wrap(embed.AddField(title, value, isInline)); - - public IEmbedBuilder WithFooter(string text, string iconUrl = null) - => Wrap(embed.WithFooter(text, iconUrl)); - - public IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) - => Wrap(embed.WithAuthor(name, iconUrl, url)); - - public IEmbedBuilder WithUrl(string url) - => Wrap(embed.WithUrl(url)); - - public IEmbedBuilder WithImageUrl(string url) - => Wrap(embed.WithImageUrl(url)); - - public IEmbedBuilder WithThumbnailUrl(string url) - => Wrap(embed.WithThumbnailUrl(url)); - - public IEmbedBuilder WithColor(EmbedColor color) - => color switch - { - EmbedColor.Ok => Wrap(embed.WithColor(_botConfig.Color.Ok.ToDiscordColor())), - EmbedColor.Pending => Wrap(embed.WithColor(_botConfig.Color.Pending.ToDiscordColor())), - EmbedColor.Error => Wrap(embed.WithColor(_botConfig.Color.Error.ToDiscordColor())), - _ => throw new ArgumentOutOfRangeException(nameof(color), "Unsupported EmbedColor type") - }; - - public IEmbedBuilder WithDiscordColor(Color color) - => Wrap(embed.WithColor(color)); - - public Embed Build() - => embed.Build(); - - private IEmbedBuilder Wrap(EmbedBuilder eb) - { - embed = eb; - return this; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IGoogleApiService.cs b/src/Ellie.Bot.Common/Services/IGoogleApiService.cs deleted file mode 100644 index 36eec7c..0000000 --- a/src/Ellie.Bot.Common/Services/IGoogleApiService.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable - -namespace Ellie.Services; - -public interface IGoogleApiService -{ - IReadOnlyDictionary Languages { get; } - - Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1); - Task> GetVideoInfosByKeywordAsync(string keywords, int count = 1); - Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1); - Task> GetRelatedVideosAsync(string id, int count = 1, string user = null); - Task> GetPlaylistTracksAsync(string playlistId, int count = 50); - Task> GetVideoDurationsAsync(IEnumerable videoIds); - Task Translate(string sourceText, string sourceLanguage, string targetLanguage); - - Task ShortenUrl(string url); - Task ShortenUrl(Uri url); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ILocalDataCache.cs b/src/Ellie.Bot.Common/Services/ILocalDataCache.cs deleted file mode 100644 index d975f04..0000000 --- a/src/Ellie.Bot.Common/Services/ILocalDataCache.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using Ellie.Common.Pokemon; -using Ellie.Modules.Games.Common.Trivia; - -namespace Ellie.Services; - -public interface ILocalDataCache -{ - Task> GetPokemonsAsync(); - Task> GetPokemonAbilitiesAsync(); - Task GetTriviaQuestionsAsync(); - Task> GetPokemonMapAsync(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ILocalization.cs b/src/Ellie.Bot.Common/Services/ILocalization.cs deleted file mode 100644 index 94530fd..0000000 --- a/src/Ellie.Bot.Common/Services/ILocalization.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using System.Globalization; - -namespace Ellie.Services; - -public interface ILocalization -{ - CultureInfo DefaultCultureInfo { get; } - IDictionary GuildCultureInfos { get; } - - CultureInfo GetCultureInfo(IGuild guild); - CultureInfo GetCultureInfo(ulong? guildId); - void RemoveGuildCulture(IGuild guild); - void RemoveGuildCulture(ulong guildId); - void ResetDefaultCulture(); - void SetDefaultCulture(CultureInfo ci); - void SetGuildCulture(IGuild guild, CultureInfo ci); - void SetGuildCulture(ulong guildId, CultureInfo ci); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IRemindService.cs b/src/Ellie.Bot.Common/Services/IRemindService.cs deleted file mode 100644 index dd90328..0000000 --- a/src/Ellie.Bot.Common/Services/IRemindService.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Utility.Services; - -public interface IRemindService -{ - Task AddReminderAsync(ulong userId, - ulong targetId, - ulong? guildId, - bool isPrivate, - DateTime time, - string message, - ReminderType reminderType); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/IStatsService.cs b/src/Ellie.Bot.Common/Services/IStatsService.cs deleted file mode 100644 index a3b798a..0000000 --- a/src/Ellie.Bot.Common/Services/IStatsService.cs +++ /dev/null @@ -1,51 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public interface IStatsService -{ - /// - /// The author of the bot. - /// - string Author { get; } - - /// - /// The total amount of commands ran since startup. - /// - long CommandsRan { get; } - - /// - /// The amount of messages seen by the bot since startup. - /// - long MessageCounter { get; } - - /// - /// The rate of messages the bot sees every second. - /// - double MessagesPerSecond { get; } - - /// - /// The total amount of text channels the bot can see. - /// - long TextChannels { get; } - - /// - /// The total amount of voice channels the bot can see. - /// - long VoiceChannels { get; } - - /// - /// Gets for how long the bot has been up since startup. - /// - TimeSpan GetUptime(); - - /// - /// Gets a formatted string of how long the bot has been up since startup. - /// - /// The formatting separator. - string GetUptimeString(string separator = ", "); - - /// - /// Gets total amount of private memory currently in use by the bot, in Megabytes. - /// - double GetPrivateMemoryMegabytes(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/ITimezoneService.cs b/src/Ellie.Bot.Common/Services/ITimezoneService.cs deleted file mode 100644 index a587d6f..0000000 --- a/src/Ellie.Bot.Common/Services/ITimezoneService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Common; - -public interface ITimezoneService -{ - TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/BehaviorExecutor.cs b/src/Ellie.Bot.Common/Services/Impl/BehaviorExecutor.cs deleted file mode 100644 index 58b5df5..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/BehaviorExecutor.cs +++ /dev/null @@ -1,302 +0,0 @@ -#nullable disable -using Microsoft.Extensions.DependencyInjection; -using Ellie.Common.ModuleBehaviors; - -namespace Ellie.Services; - -// should be renamed to handler as it's not only executing -public sealed class BehaviorHandler : IBehaviorHandler -{ - private readonly IServiceProvider _services; - - private IReadOnlyCollection noCommandExecs; - private IReadOnlyCollection preCommandExecs; - private IReadOnlyCollection onMessageExecs; - private IReadOnlyCollection inputTransformers; - - private readonly SemaphoreSlim _customLock = new(1, 1); - private readonly List _customExecs = new(); - - public BehaviorHandler(IServiceProvider services) - { - _services = services; - } - - public void Initialize() - { - noCommandExecs = _services.GetServices().ToArray(); - preCommandExecs = _services.GetServices().OrderByDescending(x => x.Priority).ToArray(); - onMessageExecs = _services.GetServices().OrderByDescending(x => x.Priority).ToArray(); - inputTransformers = _services.GetServices().ToArray(); - } - - #region Add/Remove - - public async Task AddRangeAsync(IEnumerable execs) - { - await _customLock.WaitAsync(); - try - { - foreach (var exe in execs) - { - if (_customExecs.Contains(exe)) - continue; - - _customExecs.Add(exe); - } - } - finally - { - _customLock.Release(); - } - } - - public async Task AddAsync(ICustomBehavior behavior) - { - await _customLock.WaitAsync(); - try - { - if (_customExecs.Contains(behavior)) - return false; - - _customExecs.Add(behavior); - return true; - } - finally - { - _customLock.Release(); - } - } - - public async Task RemoveAsync(ICustomBehavior behavior) - { - await _customLock.WaitAsync(); - try - { - return _customExecs.Remove(behavior); - } - finally - { - _customLock.Release(); - } - } - - public async Task RemoveRangeAsync(IEnumerable behs) - { - await _customLock.WaitAsync(); - try - { - foreach(var beh in behs) - _customExecs.Remove(beh); - } - finally - { - _customLock.Release(); - } - } - - #endregion - - #region Running - - public async Task RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg) - { - async Task Exec(IReadOnlyCollection execs) - where T : IExecOnMessage - { - foreach (var exec in execs) - { - try - { - if (await exec.ExecOnMessageAsync(guild, usrMsg)) - { - Log.Information("{TypeName} blocked message g:{GuildId} u:{UserId} c:{ChannelId} msg:{Message}", - GetExecName(exec), - guild?.Id, - usrMsg.Author.Id, - usrMsg.Channel.Id, - usrMsg.Content?.TrimTo(10)); - - return true; - } - } - catch (Exception ex) - { - Log.Error(ex, - "An error occurred in {TypeName} late blocker: {ErrorMessage}", - GetExecName(exec), - ex.Message); - } - } - - return false; - } - - if (await Exec(onMessageExecs)) - { - return true; - } - - await _customLock.WaitAsync(); - try - { - if (await Exec(_customExecs)) - return true; - } - finally - { - _customLock.Release(); - } - - return false; - } - - private string GetExecName(IBehavior exec) - => exec.Name; - - public async Task RunPreCommandAsync(ICommandContext ctx, CommandInfo cmd) - { - async Task Exec(IReadOnlyCollection execs) where T: IExecPreCommand - { - foreach (var exec in execs) - { - try - { - if (await exec.ExecPreCommandAsync(ctx, cmd.Module.GetTopLevelModule().Name, cmd)) - { - Log.Information("{TypeName} Pre-Command blocked [{User}] Command: [{Command}]", - GetExecName(exec), - ctx.User, - cmd.Aliases[0]); - return true; - } - } - catch (Exception ex) - { - Log.Error(ex, - "An error occurred in {TypeName} PreCommand: {ErrorMessage}", - GetExecName(exec), - ex.Message); - } - } - - return false; - } - - if (await Exec(preCommandExecs)) - return true; - - await _customLock.WaitAsync(); - try - { - if (await Exec(_customExecs)) - return true; - } - finally - { - _customLock.Release(); - } - - return false; - } - - public async Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg) - { - async Task Exec(IReadOnlyCollection execs) where T : IExecNoCommand - { - foreach (var exec in execs) - { - try - { - await exec.ExecOnNoCommandAsync(guild, usrMsg); - } - catch (Exception ex) - { - Log.Error(ex, - "An error occurred in {TypeName} OnNoCommand: {ErrorMessage}", - GetExecName(exec), - ex.Message); - } - } - } - - await Exec(noCommandExecs); - - await _customLock.WaitAsync(); - try - { - await Exec(_customExecs); - } - finally - { - _customLock.Release(); - } - } - - public async Task RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg) - { - async Task Exec(IReadOnlyCollection execs, string content) - where T : IInputTransformer - { - foreach (var exec in execs) - { - try - { - var newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, content); - if (newContent is not null) - { - Log.Information("{ExecName} transformed content {OldContent} -> {NewContent}", - GetExecName(exec), - content, - newContent); - return newContent; - } - } - catch (Exception ex) - { - Log.Warning(ex, "An error occured during InputTransform handling: {ErrorMessage}", ex.Message); - } - } - - return null; - } - - var newContent = await Exec(inputTransformers, usrMsg.Content); - if (newContent is not null) - return newContent; - - await _customLock.WaitAsync(); - try - { - newContent = await Exec(_customExecs, usrMsg.Content); - if (newContent is not null) - return newContent; - } - finally - { - _customLock.Release(); - } - - return usrMsg.Content; - } - - public async ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd) - { - foreach (var exec in _customExecs) - { - try - { - await exec.ExecPostCommandAsync(ctx, moduleName, cmd.Name); - } - catch (Exception ex) - { - Log.Warning(ex, - "An error occured during PostCommand handling in {ExecName}: {ErrorMessage}", - GetExecName(exec), - ex.Message); - } - } - } - - #endregion -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/BlacklistService.cs b/src/Ellie.Bot.Common/Services/Impl/BlacklistService.cs deleted file mode 100644 index 6baa752..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/BlacklistService.cs +++ /dev/null @@ -1,136 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions.Services; - -public sealed class BlacklistService : IExecOnMessage -{ - public int Priority - => int.MaxValue; - - private readonly DbService _db; - private readonly IPubSub _pubSub; - private readonly IBotCredentials _creds; - private IReadOnlyList blacklist; - - private readonly TypedKey _blPubKey = new("blacklist.reload"); - - public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds) - { - _db = db; - _pubSub = pubSub; - _creds = creds; - - Reload(false); - _pubSub.Sub(_blPubKey, OnReload); - } - - private ValueTask OnReload(BlacklistEntry[] newBlacklist) - { - blacklist = newBlacklist; - return default; - } - - public Task ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg) - { - foreach (var bl in blacklist) - { - if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id) - { - Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id); - - return Task.FromResult(true); - } - - if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id) - { - Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]", - usrMsg.Channel.Name, - usrMsg.Channel.Id); - - return Task.FromResult(true); - } - - if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id) - { - Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]", - usrMsg.Author.ToString(), - usrMsg.Author.Id); - - return Task.FromResult(true); - } - } - - return Task.FromResult(false); - } - - public IReadOnlyList GetBlacklist() - => blacklist; - - public void Reload(bool publish = true) - { - using var uow = _db.GetDbContext(); - var toPublish = uow.GetTable().ToArray(); - blacklist = toPublish; - if (publish) - _pubSub.Pub(_blPubKey, toPublish); - } - - public async Task Blacklist(BlacklistType type, ulong id) - { - if (_creds.OwnerIds.Contains(id)) - return; - - await using var uow = _db.GetDbContext(); - - await uow - .GetTable() - .InsertAsync(() => new() - { - ItemId = id, - Type = type, - }); - - Reload(); - } - - public async Task UnBlacklist(BlacklistType type, ulong id) - { - await using var uow = _db.GetDbContext(); - await uow.GetTable() - .Where(bi => bi.ItemId == id && bi.Type == type) - .DeleteAsync(); - - Reload(); - } - - public void BlacklistUsers(IReadOnlyCollection toBlacklist) - { - using (var uow = _db.GetDbContext()) - { - var bc = uow.Set(); - bc.AddRange(toBlacklist.Select(x => new BlacklistEntry - { - ItemId = x, - Type = BlacklistType.User - })); - - // todo check if blacklist works and removes currency - uow.GetTable() - .UpdateAsync(x => toBlacklist.Contains(x.UserId), - _ => new() - { - CurrencyAmount = 0 - }); - - uow.SaveChanges(); - } - - Reload(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/CommandsUtilityService.cs b/src/Ellie.Bot.Common/Services/Impl/CommandsUtilityService.cs deleted file mode 100644 index feceb16..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/CommandsUtilityService.cs +++ /dev/null @@ -1,172 +0,0 @@ -using CommandLine; - -namespace Ellie.Common; - -public sealed class CommandsUtilityService : ICommandsUtilityService, IEService -{ - private readonly CommandHandler _ch; - private readonly IBotStrings _strings; - private readonly DiscordPermOverrideService _dpos; - private readonly IEmbedBuilderService _eb; - private readonly ILocalization _loc; - private readonly Ellie.Marmalade.IMarmaladeLoaderSevice _marmalades; - - public CommandsUtilityService( - CommandHandler ch, - IBotStrings strings, - DiscordPermOverrideService dpos, - IEmbedBuilderService eb, - ILocalization loc, - Ellie.Marmalade.IMarmaladeLoaderSevice marmalades) - { - _ch = ch; - _strings = strings; - _dpos = dpos; - _eb = eb; - _loc = loc; - _marmalades = marmalades; - } - - public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild) - { - var prefix = _ch.GetPrefix(guild); - - var str = $"**`{prefix + com.Aliases.First()}`**"; - var alias = com.Aliases.Skip(1).FirstOrDefault(); - if (alias is not null) - str += $" **/ `{prefix + alias}`**"; - - var culture = _loc.GetCultureInfo(guild); - - var em = _eb.Create() - .AddField(str, $"{com.RealSummary(_strings, _marmalades, culture, prefix)}", true); - - _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides); - var reqs = GetCommandRequirements(com, (GuildPermission?)overrides); - if (reqs.Any()) - em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); - - em.AddField(_strings.GetText(strs.usage), - string.Join("\n", com.RealRemarksArr(_strings, _marmalades, culture, prefix).Map(arg => Format.Code(arg)))) - .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)) - .WithOkColor(); - - var opt = GetEllieOptionType(com.Attributes); - if (opt is not null) - { - var hs = GetCommandOptionHelp(opt); - if (!string.IsNullOrWhiteSpace(hs)) - em.AddField(GetText(strs.options, guild), hs); - } - - return em; - } - - public static string GetCommandOptionHelp(Type opt) - { - var strs = GetCommandOptionHelpList(opt); - - return string.Join("\n", strs); - } - - public static List GetCommandOptionHelpList(Type opt) - { - var strs = opt.GetProperties() - .Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute)) - .Where(x => x is not null) - .Cast() - .Select(x => - { - var toReturn = $"`--{x.LongName}`"; - - if (!string.IsNullOrWhiteSpace(x.ShortName)) - toReturn += $" (`-{x.ShortName}`)"; - - toReturn += $" {x.HelpText} "; - return toReturn; - }) - .ToList(); - - return strs; - } - - public static Type? GetEllieOptionType(IEnumerable attributes) - => attributes - .Select(a => a.GetType()) - .Where(a => a.IsGenericType - && a.GetGenericTypeDefinition() == typeof(EllieOptionsAttribute<>)) - .Select(a => a.GenericTypeArguments[0]) - .FirstOrDefault(); - - public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null) - { - var toReturn = new List(); - - 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 userPermString = cmd.Preconditions - .Where(ca => ca is UserPermAttribute) - .Cast() - .Select(userPerm => - { - if (userPerm.ChannelPermission is { } cPerm) - return GetPreconditionString(cPerm); - - if (userPerm.GuildPermission is { } gPerm) - return GetPreconditionString(gPerm); - - return string.Empty; - }) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Join('\n'); - - if (overrides is null) - { - if (!string.IsNullOrWhiteSpace(userPermString)) - toReturn.Add(userPermString); - } - else - { - if (!string.IsNullOrWhiteSpace(userPermString)) - toReturn.Add(Format.Strikethrough(userPermString)); - - toReturn.Add(GetPreconditionString(overrides.Value)); - } - - return toReturn.ToArray(); - } - - public static string GetPreconditionString(ChannelPerm perm) - => (perm + " Channel Permission").Replace("Guild", "Server"); - - public static string GetPreconditionString(GuildPerm perm) - => (perm + " Server Permission").Replace("Guild", "Server"); - - public string GetText(LocStr str, IGuild? guild) - => _strings.GetText(str, guild?.Id); -} - -public interface ICommandsUtilityService -{ - IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/DiscordPermOverrideService.cs b/src/Ellie.Bot.Common/Services/Impl/DiscordPermOverrideService.cs deleted file mode 100644 index dd5817b..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/DiscordPermOverrideService.cs +++ /dev/null @@ -1,136 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Services.Database.Models; - -namespace Ellie.Services; - -public class DiscordPermOverrideService : IEService, IExecPreCommand, IDiscordPermOverrideService -{ - public int Priority { get; } = int.MaxValue; - private readonly DbService _db; - private readonly IServiceProvider _services; - - private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides; - - public DiscordPermOverrideService(DbService db, IServiceProvider services) - { - _db = db; - _services = services; - using var uow = _db.GetDbContext(); - _overrides = uow.Set() - .AsNoTracking() - .AsEnumerable() - .ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o) - .ToConcurrent(); - } - - public bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm) - { - commandName = commandName.ToLowerInvariant(); - if (_overrides.TryGetValue((guildId, commandName), out var dpo)) - { - perm = dpo.Perm; - return true; - } - - perm = null; - return false; - } - - public Task ExecuteOverrides( - ICommandContext ctx, - CommandInfo command, - GuildPerm perms, - IServiceProvider services) - { - var rupa = new RequireUserPermissionAttribute(perms); - return rupa.CheckPermissionsAsync(ctx, command, services); - } - - public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm) - { - commandName = commandName.ToLowerInvariant(); - await using var uow = _db.GetDbContext(); - var over = await uow.Set() - .AsQueryable() - .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command); - - if (over is null) - { - uow.Set() - .Add(over = new() - { - Command = commandName, - Perm = (Ellie.Bot.Db.GuildPerm)perm, - GuildId = guildId - }); - } - else - over.Perm = (Ellie.Bot.Db.GuildPerm)perm; - - _overrides[(guildId, commandName)] = over; - - await uow.SaveChangesAsync(); - } - - public async Task ClearAllOverrides(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var overrides = await uow.Set() - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .ToListAsync(); - - uow.RemoveRange(overrides); - await uow.SaveChangesAsync(); - - foreach (var over in overrides) - _overrides.TryRemove((guildId, over.Command), out _); - } - - public async Task RemoveOverride(ulong guildId, string commandName) - { - commandName = commandName.ToLowerInvariant(); - - await using var uow = _db.GetDbContext(); - var over = await uow.Set() - .AsQueryable() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName); - - if (over is null) - return; - - uow.Remove(over); - await uow.SaveChangesAsync(); - - _overrides.TryRemove((guildId, commandName), out _); - } - - public async Task> GetAllOverrides(ulong guildId) - { - await using var uow = _db.GetDbContext(); - return await uow.Set() - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .ToListAsync(); - } - - public async Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command) - { - if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null) - { - var result = - await new RequireUserPermissionAttribute((GuildPermission)perm).CheckPermissionsAsync(context, - command, - _services); - - return !result.IsSuccess; - } - - return false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/FontProvider.cs b/src/Ellie.Bot.Common/Services/Impl/FontProvider.cs deleted file mode 100644 index 0777e8c..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/FontProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -#nullable disable -using SixLabors.Fonts; - -namespace Ellie.Services; - -public class FontProvider : IEService -{ - public FontFamily DottyFont { get; } - - public FontFamily UniSans { get; } - - public FontFamily NotoSans { get; } - //public FontFamily Emojis { get; } - - /// - /// Font used for .rip command - /// - public Font RipFont { get; } - - public List FallBackFonts { get; } - private readonly FontCollection _fonts; - - public FontProvider() - { - _fonts = new(); - - NotoSans = _fonts.Add("data/fonts/NotoSans-Bold.ttf"); - UniSans = _fonts.Add("data/fonts/Uni Sans.ttf"); - - FallBackFonts = new(); - - //FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf")); - - // try loading some emoji and jap fonts on windows as fallback fonts - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - try - { - var fontsfolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); - FallBackFonts.Add(_fonts.Add(Path.Combine(fontsfolder, "seguiemj.ttf"))); - FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "msgothic.ttc"))); - FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "segoe.ttc"))); - } - catch { } - } - - // any fonts present in data/fonts should be added as fallback fonts - // this will allow support for special characters when drawing text - foreach (var font in Directory.GetFiles(@"data/fonts")) - { - if (font.EndsWith(".ttf")) - FallBackFonts.Add(_fonts.Add(font)); - else if (font.EndsWith(".ttc")) - FallBackFonts.AddRange(_fonts.AddCollection(font)); - } - - RipFont = NotoSans.CreateFont(20, FontStyle.Bold); - DottyFont = FallBackFonts.First(x => x.Name == "dotty"); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/IImageCache.cs b/src/Ellie.Bot.Common/Services/Impl/IImageCache.cs deleted file mode 100644 index 28ff952..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/IImageCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Ellie.Services; - -public interface IImageCache -{ - Task GetHeadsImageAsync(); - Task GetTailsImageAsync(); - Task GetCurrencyImageAsync(); - Task GetXpBackgroundImageAsync(); - Task GetRategirlBgAsync(); - Task GetRategirlDotAsync(); - Task GetDiceAsync(int num); - Task GetSlotEmojiAsync(int number); - Task GetSlotBgAsync(); - Task GetRipBgAsync(); - Task GetRipOverlayAsync(); - Task GetImageDataAsync(Uri url); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/ImagesConfig.cs b/src/Ellie.Bot.Common/Services/Impl/ImagesConfig.cs deleted file mode 100644 index 8546c7c..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/ImagesConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Ellie.Common.Configs; - -namespace Ellie.Services; - -public sealed class ImagesConfig : ConfigServiceBase -{ - private const string PATH = "data/images.yml"; - - private static readonly TypedKey _changeKey = - new("config.images.updated"); - - public override string Name - => "images"; - - public ImagesConfig(IConfigSeria serializer, IPubSub pubSub) - : base(PATH, serializer, pubSub, _changeKey) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/RedisImageExtensions.cs b/src/Ellie.Bot.Common/Services/Impl/RedisImageExtensions.cs deleted file mode 100644 index 721f6b7..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/RedisImageExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public static class RedisImageExtensions -{ - private const string OLD_CDN_URL = "ellie.nyc3.digitaloceanspaces.com"; - private const string NEW_CDN_URL = "ellie.gcoms.xyz"; - - public static Uri ToNewCdn(this Uri uri) - => new(uri.ToString().Replace(OLD_CDN_URL, NEW_CDN_URL)); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/SingleProcessCoordinator.cs b/src/Ellie.Bot.Common/Services/Impl/SingleProcessCoordinator.cs deleted file mode 100644 index 49983cc..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/SingleProcessCoordinator.cs +++ /dev/null @@ -1,58 +0,0 @@ -#nullable disable -using System.Diagnostics; - -namespace Ellie.Services; - -public class SingleProcessCoordinator : ICoordinator -{ - private readonly IBotCredentials _creds; - private readonly DiscordSocketClient _client; - - public SingleProcessCoordinator(IBotCredentials creds, DiscordSocketClient client) - { - _creds = creds; - _client = client; - } - - public bool RestartBot() - { - if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd) - || string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args)) - { - Log.Error("You must set RestartCommand.Cmd and RestartCommand.Args in creds.yml"); - return false; - } - - Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args); - _ = Task.Run(async () => - { - await Task.Delay(2000); - Die(); - }); - return true; - } - - public void Die(bool graceful = false) - => Environment.Exit(5); - - public bool RestartShard(int shardId) - => RestartBot(); - - public IList GetAllShardStatuses() - => new[] - { - new ShardStatus - { - ConnectionState = _client.ConnectionState, - GuildCount = _client.Guilds.Count, - LastUpdate = DateTime.UtcNow, - ShardId = _client.ShardId - } - }; - - public int GetGuildCount() - => _client.Guilds.Count; - - public Task Reload() - => Task.CompletedTask; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/StartingGuildsListService.cs b/src/Ellie.Bot.Common/Services/Impl/StartingGuildsListService.cs deleted file mode 100644 index fc6e541..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/StartingGuildsListService.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable disable -using System.Collections; - -namespace Ellie.Services; - -public class StartingGuildsService : IEnumerable, IEService -{ - private readonly IReadOnlyList _guilds; - - public StartingGuildsService(DiscordSocketClient client) - => _guilds = client.Guilds.Select(x => x.Id).ToList(); - - public IEnumerator GetEnumerator() - => _guilds.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => _guilds.GetEnumerator(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/Impl/StatsService.cs b/src/Ellie.Bot.Common/Services/Impl/StatsService.cs deleted file mode 100644 index 2e3ef10..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/StatsService.cs +++ /dev/null @@ -1,188 +0,0 @@ -#nullable disable -using Humanizer.Localisation; -using Ellie.Common.ModuleBehaviors; -using System.Diagnostics; - -namespace Ellie.Services; - -public sealed class StatsService : IStatsService, IReadyExecutor, IEService -{ - public const string BOT_VERSION = "5.0.0-alpha1"; - - public string Author - => "toastie_t0ast"; - - public double MessagesPerSecond - => MessageCounter / GetUptime().TotalSeconds; - - public long TextChannels - => Interlocked.Read(ref textChannels); - - public long VoiceChannels - => Interlocked.Read(ref voiceChannels); - - public long MessageCounter - => Interlocked.Read(ref messageCounter); - - public long CommandsRan - => Interlocked.Read(ref commandsRan); - - private readonly Process _currentProcess = Process.GetCurrentProcess(); - private readonly DiscordSocketClient _client; - private readonly IBotCredentials _creds; - private readonly DateTime _started; - - private long textChannels; - private long voiceChannels; - private long messageCounter; - private long commandsRan; - - private readonly IHttpClientFactory _httpFactory; - - public StatsService( - DiscordSocketClient client, - CommandHandler cmdHandler, - IBotCredentials creds, - IHttpClientFactory factory) - { - _client = client; - _creds = creds; - _httpFactory = factory; - - _started = DateTime.UtcNow; - _client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref messageCounter)); - cmdHandler.CommandExecuted += (_, _) => Task.FromResult(Interlocked.Increment(ref commandsRan)); - - _client.ChannelCreated += c => - { - _ = Task.Run(() => - { - if (c is ITextChannel) - Interlocked.Increment(ref textChannels); - else if (c is IVoiceChannel) - Interlocked.Increment(ref voiceChannels); - }); - - return Task.CompletedTask; - }; - - _client.ChannelDestroyed += c => - { - _ = Task.Run(() => - { - if (c is ITextChannel) - Interlocked.Decrement(ref textChannels); - else if (c is IVoiceChannel) - Interlocked.Decrement(ref voiceChannels); - }); - - return Task.CompletedTask; - }; - - _client.GuildAvailable += g => - { - _ = Task.Run(() => - { - var tc = g.Channels.Count(cx => cx is ITextChannel); - var vc = g.Channels.Count - tc; - Interlocked.Add(ref textChannels, tc); - Interlocked.Add(ref voiceChannels, vc); - }); - return Task.CompletedTask; - }; - - _client.JoinedGuild += g => - { - _ = Task.Run(() => - { - var tc = g.Channels.Count(cx => cx is ITextChannel); - var vc = g.Channels.Count - tc; - Interlocked.Add(ref textChannels, tc); - Interlocked.Add(ref voiceChannels, vc); - }); - return Task.CompletedTask; - }; - - _client.GuildUnavailable += g => - { - _ = Task.Run(() => - { - var tc = g.Channels.Count(cx => cx is ITextChannel); - var vc = g.Channels.Count - tc; - Interlocked.Add(ref textChannels, -tc); - Interlocked.Add(ref voiceChannels, -vc); - }); - - return Task.CompletedTask; - }; - - _client.LeftGuild += g => - { - _ = Task.Run(() => - { - var tc = g.Channels.Count(cx => cx is ITextChannel); - var vc = g.Channels.Count - tc; - Interlocked.Add(ref textChannels, -tc); - Interlocked.Add(ref voiceChannels, -vc); - }); - - return Task.CompletedTask; - }; - } - - private void InitializeChannelCount() - { - var guilds = _client.Guilds; - textChannels = guilds.Sum(static g => g.Channels.Count(static cx => cx is ITextChannel)); - voiceChannels = guilds.Sum(static g => g.Channels.Count(static cx => cx is IVoiceChannel)); - } - - public async Task OnReadyAsync() - { - InitializeChannelCount(); - - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - do - { - if (string.IsNullOrWhiteSpace(_creds.BotListToken)) - continue; - - try - { - using var http = _httpFactory.CreateClient(); - using var content = new FormUrlEncodedContent(new Dictionary - { - { "shard_count", _creds.TotalShards.ToString() }, - { "shard_id", _client.ShardId.ToString() }, - { "server_count", _client.Guilds.Count().ToString() } - }); - content.Headers.Clear(); - content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); - http.DefaultRequestHeaders.Add("Authorization", _creds.BotListToken); - - using var res = await http.PostAsync( - new Uri($"https://discordbots.org/api/bots/{_client.CurrentUser.Id}/stats"), - content); - } - catch (Exception ex) - { - Log.Error(ex, "Error in botlist post"); - } - } while (await timer.WaitForNextTickAsync()); - } - - public TimeSpan GetUptime() - => DateTime.UtcNow - _started; - - public string GetUptimeString(string separator = ", ") - { - var time = GetUptime(); - return time.Humanize(3, maxUnit: TimeUnit.Day, minUnit: TimeUnit.Minute); - } - - public double GetPrivateMemoryMegabytes() - { - _currentProcess.Refresh(); - return _currentProcess.PrivateMemorySize64 / 1.Megabytes().Bytes; - } -} diff --git a/src/Ellie.Bot.Common/Services/Impl/YtdlOperation.cs b/src/Ellie.Bot.Common/Services/Impl/YtdlOperation.cs deleted file mode 100644 index dfc3ab4..0000000 --- a/src/Ellie.Bot.Common/Services/Impl/YtdlOperation.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable disable -using System.ComponentModel; -using System.Diagnostics; -using System.Text; - -namespace Ellie.Services; - -public class YtdlOperation -{ - private readonly string _baseArgString; - private readonly bool _isYtDlp; - - public YtdlOperation(string baseArgString, bool isYtDlp = false) - { - _baseArgString = baseArgString; - _isYtDlp = isYtDlp; - } - - private Process CreateProcess(string[] args) - { - var newArgs = args.Map(arg => (object)arg.Replace("\"", "")); - return new() - { - StartInfo = new() - { - FileName = _isYtDlp ? "yt-dlp" : "youtube-dl", - Arguments = string.Format(_baseArgString, newArgs), - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - CreateNoWindow = true - } - }; - } - - public async Task GetDataAsync(params string[] args) - { - try - { - using var process = CreateProcess(args); - - Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - process.Start(); - - var str = await process.StandardOutput.ReadToEndAsync(); - var err = await process.StandardError.ReadToEndAsync(); - if (!string.IsNullOrEmpty(err)) - Log.Warning("YTDL warning: {YtdlWarning}", err); - - return str; - } - catch (Win32Exception) - { - Log.Error("youtube-dl is likely not installed. " + "Please install it before running the command again"); - return default; - } - catch (Exception ex) - { - Log.Error(ex, "Exception running youtube-dl: {ErrorMessage}", ex.Message); - return default; - } - } - - public async IAsyncEnumerable EnumerateDataAsync(params string[] args) - { - using var process = CreateProcess(args); - - Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - process.Start(); - - string line; - while ((line = await process.StandardOutput.ReadLineAsync()) is not null) - yield return line; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/strings/impl/BotStrings.cs b/src/Ellie.Bot.Common/Services/strings/impl/BotStrings.cs deleted file mode 100644 index 9f96cdc..0000000 --- a/src/Ellie.Bot.Common/Services/strings/impl/BotStrings.cs +++ /dev/null @@ -1,101 +0,0 @@ -#nullable disable -using System.Globalization; - -namespace Ellie.Services; - -public class BotStrings : IBotStrings -{ - /// - /// Used as failsafe in case response key doesn't exist in the selected or default language. - /// - private readonly CultureInfo _usCultureInfo = new("en-US"); - - private readonly ILocalization _localization; - private readonly IBotStringsProvider _stringsProvider; - - public BotStrings(ILocalization loc, IBotStringsProvider stringsProvider) - { - _localization = loc; - _stringsProvider = stringsProvider; - } - - private string GetString(string key, CultureInfo cultureInfo) - => _stringsProvider.GetText(cultureInfo.Name, key); - - public string GetText(string key, ulong? guildId = null, params object[] data) - => GetText(key, _localization.GetCultureInfo(guildId), data); - - public string GetText(string key, CultureInfo cultureInfo) - { - var text = GetString(key, cultureInfo); - - if (string.IsNullOrWhiteSpace(text)) - { - Log.Warning("'{Key}' key is missing from '{LanguageName}' response strings. You may ignore this message", - key, - cultureInfo.Name); - text = GetString(key, _usCultureInfo) ?? $"Error: dkey {key} not found!"; - if (string.IsNullOrWhiteSpace(text)) - { - return - "I can't tell you if the command is executed, because there was an error printing out the response." - + $" Key '{key}' is missing from resources. You may ignore this message."; - } - } - - return text; - } - - public string GetText(string key, CultureInfo cultureInfo, params object[] data) - { - try - { - return string.Format(GetText(key, cultureInfo), data); - } - catch (FormatException) - { - Log.Warning( - " Key '{Key}' is not properly formatted in '{LanguageName}' response strings. Please report this", - key, - cultureInfo.Name); - if (cultureInfo.Name != _usCultureInfo.Name) - return GetText(key, _usCultureInfo, data); - return - "I can't tell you if the command is executed, because there was an error printing out the response.\n" - + $"Key '{key}' is not properly formatted. Please report this."; - } - } - - public CommandStrings GetCommandStrings(string commandName, ulong? guildId = null) - => GetCommandStrings(commandName, _localization.GetCultureInfo(guildId)); - - public CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo) - { - var cmdStrings = _stringsProvider.GetCommandStrings(cultureInfo.Name, commandName); - if (cmdStrings is null) - { - if (cultureInfo.Name == _usCultureInfo.Name) - { - Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings. Please report this", - commandName); - - return new CommandStrings() - { - Args = new[] { "" }, - Desc = "?" - }; - } - -// Log.Warning(@"'{CommandName}' command strings don't exist in '{LanguageName}' culture. -// This message is safe to ignore, however you can ask in Ellie support server how you can contribute command translations", -// commandName, cultureInfo.Name); - - return GetCommandStrings(commandName, _usCultureInfo); - } - - return cmdStrings; - } - - public void Reload() - => _stringsProvider.Reload(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/strings/impl/LocalFileStringsSource.cs b/src/Ellie.Bot.Common/Services/strings/impl/LocalFileStringsSource.cs deleted file mode 100644 index 56e4f28..0000000 --- a/src/Ellie.Bot.Common/Services/strings/impl/LocalFileStringsSource.cs +++ /dev/null @@ -1,73 +0,0 @@ -#nullable disable -using Newtonsoft.Json; -using YamlDotNet.Serialization; - -namespace Ellie.Services; - -/// -/// Loads strings from the local default filepath -/// -public class LocalFileStringsSource : IStringsSource -{ - private readonly string _responsesPath = "data/strings/responses"; - private readonly string _commandsPath = "data/strings/commands"; - - public LocalFileStringsSource( - string responsesPath = "data/strings/responses", - string commandsPath = "data/strings/commands") - { - _responsesPath = responsesPath; - _commandsPath = commandsPath; - } - - public Dictionary> GetResponseStrings() - { - var outputDict = new Dictionary>(); - foreach (var file in Directory.GetFiles(_responsesPath)) - { - try - { - var langDict = JsonConvert.DeserializeObject>(File.ReadAllText(file)); - var localeName = GetLocaleName(file); - outputDict[localeName] = langDict; - } - catch (Exception ex) - { - Log.Error(ex, "Error loading {FileName} response strings: {ErrorMessage}", file, ex.Message); - } - } - - return outputDict; - } - - public Dictionary> GetCommandStrings() - { - var deserializer = new DeserializerBuilder().Build(); - - var outputDict = new Dictionary>(); - foreach (var file in Directory.GetFiles(_commandsPath)) - { - try - { - var text = File.ReadAllText(file); - var langDict = deserializer.Deserialize>(text); - var localeName = GetLocaleName(file); - outputDict[localeName] = langDict; - } - catch (Exception ex) - { - Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message); - } - } - - return outputDict; - } - - private static string GetLocaleName(string fileName) - { - fileName = Path.GetFileName(fileName); - var dotIndex = fileName.IndexOf('.') + 1; - var secondDotIndex = fileName.LastIndexOf('.'); - return fileName.Substring(dotIndex, secondDotIndex - dotIndex); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Services/strings/impl/MemoryBotStringsProvider.cs b/src/Ellie.Bot.Common/Services/strings/impl/MemoryBotStringsProvider.cs deleted file mode 100644 index 8fe9819..0000000 --- a/src/Ellie.Bot.Common/Services/strings/impl/MemoryBotStringsProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public class MemoryBotStringsProvider : IBotStringsProvider -{ - private readonly IStringsSource _source; - private IReadOnlyDictionary> responseStrings; - private IReadOnlyDictionary> commandStrings; - - public MemoryBotStringsProvider(IStringsSource source) - { - _source = source; - Reload(); - } - - public string GetText(string localeName, string key) - { - if (responseStrings.TryGetValue(localeName, out var langStrings) && langStrings.TryGetValue(key, out var text)) - return text; - - return null; - } - - public void Reload() - { - responseStrings = _source.GetResponseStrings(); - commandStrings = _source.GetCommandStrings(); - } - - public CommandStrings GetCommandStrings(string localeName, string commandName) - { - if (commandStrings.TryGetValue(localeName, out var langStrings) - && langStrings.TryGetValue(commandName, out var strings)) - return strings; - - return null; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Settings/BotConfigService.cs b/src/Ellie.Bot.Common/Settings/BotConfigService.cs deleted file mode 100644 index 740ffe4..0000000 --- a/src/Ellie.Bot.Common/Settings/BotConfigService.cs +++ /dev/null @@ -1,66 +0,0 @@ -#nullable disable -using Ellie.Common.Configs; -using SixLabors.ImageSharp.PixelFormats; - -namespace Ellie.Services; - -/// -/// Settings service for bot-wide configuration. -/// -public sealed class BotConfigService : ConfigServiceBase -{ - private const string FILE_PATH = "data/bot.yml"; - private static readonly TypedKey _changeKey = new("config.bot.updated"); - public override string Name { get; } = "bot"; - - public BotConfigService(IConfigSeria serializer, IPubSub pubSub) - : base(FILE_PATH, serializer, pubSub, _changeKey) - { - AddParsedProp("color.ok", bs => bs.Color.Ok, Rgba32.TryParseHex, ConfigPrinters.Color); - AddParsedProp("color.error", bs => bs.Color.Error, Rgba32.TryParseHex, ConfigPrinters.Color); - AddParsedProp("color.pending", bs => bs.Color.Pending, Rgba32.TryParseHex, ConfigPrinters.Color); - AddParsedProp("help.text", bs => bs.HelpText, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("help.dmtext", bs => bs.DmHelpText, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("console.type", bs => bs.ConsoleOutputType, Enum.TryParse, ConfigPrinters.ToString); - AddParsedProp("locale", bs => bs.DefaultLocale, ConfigParsers.Culture, ConfigPrinters.Culture); - AddParsedProp("prefix", bs => bs.Prefix, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("checkforupdates", bs => bs.CheckForUpdates, bool.TryParse, ConfigPrinters.ToString); - - Migrate(); - } - - private void Migrate() - { - if (data.Version < 2) - ModifyConfig(c => c.Version = 2); - - if (data.Version < 3) - { - ModifyConfig(c => - { - c.Version = 3; - c.Blocked.Modules = c.Blocked.Modules?.Select(static x - => string.Equals(x, - "ActualCustomReactions", - StringComparison.InvariantCultureIgnoreCase) - ? "ACTUALEXPRESSIONS" - : x) - .Distinct() - .ToHashSet(); - }); - } - - if (data.Version < 4) - ModifyConfig(c => - { - c.Version = 4; - c.CheckForUpdates = true; - }); - - if(data.Version < 5) - ModifyConfig(c => - { - c.Version = 5; - }); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Settings/ConfigParsers.cs b/src/Ellie.Bot.Common/Settings/ConfigParsers.cs deleted file mode 100644 index fe6a0ac..0000000 --- a/src/Ellie.Bot.Common/Settings/ConfigParsers.cs +++ /dev/null @@ -1,50 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp.PixelFormats; -using System.Globalization; - -namespace Ellie.Services; - -/// -/// Custom setting value parsers for types which don't have them by default -/// -public static class ConfigParsers -{ - /// - /// Default string parser. Passes input to output and returns true. - /// - public static bool String(string input, out string output) - { - output = input; - return true; - } - - public static bool Culture(string input, out CultureInfo output) - { - try - { - output = new(input); - return true; - } - catch - { - output = null; - return false; - } - } - - public static bool InsensitiveEnum(string input, out T output) - where T : struct - => Enum.TryParse(input, true, out output); -} - -public static class ConfigPrinters -{ - public static string ToString(TAny input) - => input.ToString(); - - public static string Culture(CultureInfo culture) - => culture.Name; - - public static string Color(Rgba32 color) - => ((uint)((color.B << 0) | (color.G << 8) | (color.R << 16))).ToString("X6"); -} diff --git a/src/Ellie.Bot.Common/Settings/ConfigServiceBase.cs b/src/Ellie.Bot.Common/Settings/ConfigServiceBase.cs deleted file mode 100644 index 05cf80e..0000000 --- a/src/Ellie.Bot.Common/Settings/ConfigServiceBase.cs +++ /dev/null @@ -1,201 +0,0 @@ -using Ellie.Common.Configs; -using Ellie.Common.Yml; -using System.Linq.Expressions; -using System.Reflection; - -namespace Ellie.Services; - -/// -/// Base service for all settings services -/// -/// Type of the settings -public abstract class ConfigServiceBase : IConfigService - where TSettings : ICloneable, new() -{ - // FUTURE config arrays are not copied - they're not protected from mutations - public TSettings Data - => data.Clone(); - - public abstract string Name { get; } - protected readonly string _filePath; - protected readonly IConfigSeria _serializer; - protected readonly IPubSub _pubSub; - private readonly TypedKey _changeKey; - - protected TSettings data; - - private readonly Dictionary> _propSetters = new(); - private readonly Dictionary> _propSelectors = new(); - private readonly Dictionary> _propPrinters = new(); - private readonly Dictionary _propComments = new(); - - /// - /// Initialized an instance of - /// - /// Path to the file where the settings are serialized/deserialized to and from - /// Serializer which will be used - /// Pubsub implementation for signaling when settings are updated - /// Key used to signal changed event - protected ConfigServiceBase( - string filePath, - IConfigSeria serializer, - IPubSub pubSub, - TypedKey changeKey) - { - _filePath = filePath; - _serializer = serializer; - _pubSub = pubSub; - _changeKey = changeKey; - - data = new(); - Load(); - _pubSub.Sub(_changeKey, OnChangePublished); - } - - private void PublishChange() - => _pubSub.Pub(_changeKey, data); - - private ValueTask OnChangePublished(TSettings newData) - { - data = newData; - OnStateUpdate(); - return default; - } - - /// - /// Loads data from disk. If file doesn't exist, it will be created with default values - /// - protected void Load() - { - // if file is deleted, regenerate it with default values - if (!File.Exists(_filePath)) - { - data = new(); - Save(); - } - - data = _serializer.Deserialize(File.ReadAllText(_filePath)); - } - - /// - /// Loads new data and publishes the new state - /// - public void Reload() - { - Load(); - _pubSub.Pub(_changeKey, data); - } - - /// - /// Doesn't do anything by default. This method will be executed after - /// is reloaded from or new data is recieved - /// from the publish event - /// - protected virtual void OnStateUpdate() - { - } - - private void Save() - { - var strData = _serializer.Serialize(data); - File.WriteAllText(_filePath, strData); - } - - protected void AddParsedProp( - string key, - Expression> selector, - SettingParser parser, - Func printer, - Func? checker = null) - { - checker ??= _ => true; - key = key.ToLowerInvariant(); - _propPrinters[key] = obj => printer((TProp)obj); - _propSelectors[key] = () => selector.Compile()(data)!; - _propSetters[key] = Magic(selector, parser, checker); - _propComments[key] = ((MemberExpression)selector.Body).Member.GetCustomAttribute()?.Comment; - } - - private Func Magic( - Expression> selector, - SettingParser parser, - Func checker) - => (target, input) => - { - if (!parser(input, out var value)) - return false; - - if (!checker(value)) - return false; - - object targetObject = target; - var expr = (MemberExpression)selector.Body; - var prop = (PropertyInfo)expr.Member; - - var expressions = new List(); - - while (true) - { - expr = expr.Expression as MemberExpression; - if (expr is null) - break; - - expressions.Add(expr); - } - - foreach (var memberExpression in expressions.AsEnumerable().Reverse()) - { - var localProp = (PropertyInfo)memberExpression.Member; - targetObject = localProp.GetValue(targetObject)!; - } - - prop.SetValue(targetObject, value, null); - return true; - }; - - public IReadOnlyList GetSettableProps() - => _propSetters.Keys.ToList(); - - public string? GetSetting(string prop) - { - prop = prop.ToLowerInvariant(); - if (!_propSelectors.TryGetValue(prop, out var selector) || !_propPrinters.TryGetValue(prop, out var printer)) - return null; - - return printer(selector()); - } - - public string? GetComment(string prop) - { - if (_propComments.TryGetValue(prop, out var comment)) - return comment; - - return null; - } - - private bool SetProperty(TSettings target, string key, string value) - => _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic) && magic(target, value); - - public bool SetSetting(string prop, string newValue) - { - var success = true; - ModifyConfig(bs => - { - success = SetProperty(bs, prop, newValue); - }); - - if (success) - PublishChange(); - - return success; - } - - public void ModifyConfig(Action action) - { - var copy = Data; - action(copy); - data = copy; - Save(); - PublishChange(); - } -} diff --git a/src/Ellie.Bot.Common/Settings/IConfigMigrator.cs b/src/Ellie.Bot.Common/Settings/IConfigMigrator.cs deleted file mode 100644 index ed2591d..0000000 --- a/src/Ellie.Bot.Common/Settings/IConfigMigrator.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -public interface IConfigMigrator -{ - public void EnsureMigrated(); -} diff --git a/src/Ellie.Bot.Common/Settings/IConfigService.cs b/src/Ellie.Bot.Common/Settings/IConfigService.cs deleted file mode 100644 index 4f72e59..0000000 --- a/src/Ellie.Bot.Common/Settings/IConfigService.cs +++ /dev/null @@ -1,46 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -/// -/// Interface that all services which deal with configs should implement -/// -public interface IConfigService -{ - /// - /// Name of the config - /// - public string Name { get; } - - /// - /// Loads new data and publishes the new state - /// - void Reload(); - - /// - /// Gets the list of props you can set - /// - /// List of props - IReadOnlyList GetSettableProps(); - - /// - /// Gets the value of the specified property - /// - /// Prop name - /// Value of the prop - string GetSetting(string prop); - - /// - /// Gets the value of the specified property - /// - /// Prop name - /// Value of the prop - string GetComment(string prop); - - /// - /// Sets the value of the specified property - /// - /// Property to set - /// Value to set the property to - /// Success - bool SetSetting(string prop, string newValue); -} diff --git a/src/Ellie.Bot.Common/Settings/SettingParser.cs b/src/Ellie.Bot.Common/Settings/SettingParser.cs deleted file mode 100644 index c33671d..0000000 --- a/src/Ellie.Bot.Common/Settings/SettingParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services; - -/// -/// Delegate which describes a parser which can convert string input into given data type -/// -/// Data type to convert string to -public delegate bool SettingParser(string input, out TData output); diff --git a/src/Ellie.Bot.Common/SmartText/SmartEmbedText.cs b/src/Ellie.Bot.Common/SmartText/SmartEmbedText.cs deleted file mode 100644 index af36e7a..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartEmbedText.cs +++ /dev/null @@ -1,184 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp.PixelFormats; -using System.Text.Json.Serialization; - -namespace Ellie; - -public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase -{ - public string Color { get; init; } = string.Empty; - - public SmartEmbedArrayElementText() - { - - } - - public SmartEmbedArrayElementText(IEmbed eb) : base(eb) - { - Color = eb.Color is { } c ? new Rgba32(c.R, c.G, c.B).ToHex() : string.Empty; - } - - protected override EmbedBuilder GetEmbedInternal() - { - var embed = base.GetEmbedInternal(); - if (Rgba32.TryParseHex(Color, out var color)) - return embed.WithColor(color.ToDiscordColor()); - - return embed; - } -} - -public sealed record SmartEmbedText : SmartEmbedTextBase -{ - public string PlainText { get; init; } - - public uint Color { get; init; } = 7458112; - - public SmartEmbedText() - { - } - - private SmartEmbedText(IEmbed eb, string? plainText = null) - : base(eb) - => (PlainText, Color) = (plainText, eb.Color?.RawValue ?? 0); - - public static SmartEmbedText FromEmbed(IEmbed eb, string? plainText = null) - => new(eb, plainText); - - protected override EmbedBuilder GetEmbedInternal() - { - var embed = base.GetEmbedInternal(); - return embed.WithColor(Color); - } -} - -public abstract record SmartEmbedTextBase : SmartText -{ - public string Title { get; init; } - public string Description { get; init; } - public string Url { get; init; } - public string Thumbnail { get; init; } - public string Image { get; init; } - - public SmartTextEmbedAuthor Author { get; init; } - public SmartTextEmbedFooter Footer { get; init; } - public SmartTextEmbedField[] Fields { get; init; } - - [JsonIgnore] - public bool IsValid - => !string.IsNullOrWhiteSpace(Title) - || !string.IsNullOrWhiteSpace(Description) - || !string.IsNullOrWhiteSpace(Url) - || !string.IsNullOrWhiteSpace(Thumbnail) - || !string.IsNullOrWhiteSpace(Image) - || (Footer is not null - && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) - || Fields is { Length: > 0 }; - - protected SmartEmbedTextBase() - { - - } - - protected SmartEmbedTextBase(IEmbed eb) - { - Title = eb.Title; - Description = eb.Description; - Url = eb.Url; - Thumbnail = eb.Thumbnail?.Url; - Image = eb.Image?.Url; - Author = eb.Author is { } ea - ? new() - { - Name = ea.Name, - Url = ea.Url, - IconUrl = ea.IconUrl - } - : null; - Footer = eb.Footer is { } ef - ? new() - { - Text = ef.Text, - IconUrl = ef.IconUrl - } - : null; - - if (eb.Fields.Length > 0) - { - Fields = eb.Fields.Select(field - => new SmartTextEmbedField - { - Inline = field.Inline, - Name = field.Name, - Value = field.Value - }) - .ToArray(); - } - } - - public EmbedBuilder GetEmbed() - => GetEmbedInternal(); - - protected virtual EmbedBuilder GetEmbedInternal() - { - var embed = new EmbedBuilder(); - - if (!string.IsNullOrWhiteSpace(Title)) - embed.WithTitle(Title); - - if (!string.IsNullOrWhiteSpace(Description)) - embed.WithDescription(Description); - - if (Url is not null && Uri.IsWellFormedUriString(Url, UriKind.Absolute)) - embed.WithUrl(Url); - - if (Footer is not null) - { - embed.WithFooter(efb => - { - efb.WithText(Footer.Text); - if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute)) - efb.WithIconUrl(Footer.IconUrl); - }); - } - - if (Thumbnail is not null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute)) - embed.WithThumbnailUrl(Thumbnail); - - if (Image is not null && Uri.IsWellFormedUriString(Image, UriKind.Absolute)) - embed.WithImageUrl(Image); - - if (Author is not null && !string.IsNullOrWhiteSpace(Author.Name)) - { - if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute)) - Author.IconUrl = null; - if (!Uri.IsWellFormedUriString(Author.Url, UriKind.Absolute)) - Author.Url = null; - - embed.WithAuthor(Author.Name, Author.IconUrl, Author.Url); - } - - if (Fields is not null) - { - foreach (var f in Fields) - { - if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value)) - embed.AddField(f.Name, f.Value, f.Inline); - } - } - - return embed; - } - - public void NormalizeFields() - { - if (Fields is { Length: > 0 }) - { - foreach (var f in Fields) - { - f.Name = f.Name.TrimTo(256); - f.Value = f.Value.TrimTo(1024); - } - } - } -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartEmbedTypeArray.cs b/src/Ellie.Bot.Common/SmartText/SmartEmbedTypeArray.cs deleted file mode 100644 index ae979b0..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartEmbedTypeArray.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie; - -public sealed record SmartEmbedTextArray : SmartText -{ - public string Content { get; set; } - public SmartEmbedArrayElementText[] Embeds { get; set; } - - [JsonIgnore] - public bool IsValid - => Embeds?.All(x => x.IsValid) ?? false; - - public EmbedBuilder[] GetEmbedBuilders() - { - if (Embeds is null) - return Array.Empty(); - - return Embeds - .Where(x => x.IsValid) - .Select(em => em.GetEmbed()) - .ToArray(); - } - - public void NormalizeFields() - { - if (Embeds is null) - return; - - foreach (var eb in Embeds) - eb.NormalizeFields(); - } -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartPlainText.cs b/src/Ellie.Bot.Common/SmartText/SmartPlainText.cs deleted file mode 100644 index 6dcaf50..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartPlainText.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie; - -public sealed record SmartPlainText : SmartText -{ - public string Text { get; init; } - - public SmartPlainText(string text) - => Text = text; - - public static implicit operator SmartPlainText(string input) - => new(input); - - public static implicit operator string(SmartPlainText input) - => input.Text; - - public override string ToString() - => Text; -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartText.cs b/src/Ellie.Bot.Common/SmartText/SmartText.cs deleted file mode 100644 index 3481b44..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartText.cs +++ /dev/null @@ -1,89 +0,0 @@ -#nullable disable -using Newtonsoft.Json.Linq; -using System.Text.Json.Serialization; - -namespace Ellie; - -public abstract record SmartText -{ - [JsonIgnore] - public bool IsEmbed - => this is SmartEmbedText; - - [JsonIgnore] - public bool IsPlainText - => this is SmartPlainText; - - [JsonIgnore] - public bool IsEmbedArray - => this is SmartEmbedTextArray; - - public static SmartText operator +(SmartText text, string input) - => text switch - { - SmartEmbedText set => set with - { - PlainText = set.PlainText + input - }, - SmartPlainText spt => new SmartPlainText(spt.Text + input), - SmartEmbedTextArray arr => arr with - { - Content = arr.Content + input - }, - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static SmartText operator +(string input, SmartText text) - => text switch - { - SmartEmbedText set => set with - { - PlainText = input + set.PlainText - }, - SmartPlainText spt => new SmartPlainText(input + spt.Text), - SmartEmbedTextArray arr => arr with - { - Content = input + arr.Content - }, - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static SmartText CreateFrom(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return new SmartPlainText(input); - - try - { - var doc = JObject.Parse(input); - var root = doc.Root; - if (root.Type == JTokenType.Object) - { - if (((JObject)root).TryGetValue("embeds", out _)) - { - var arr = root.ToObject(); - - if (arr is null) - return new SmartPlainText(input); - - arr!.NormalizeFields(); - return arr; - } - - var obj = root.ToObject(); - - if (obj is null || !(obj.IsValid || !string.IsNullOrWhiteSpace(obj.PlainText))) - return new SmartPlainText(input); - - obj.NormalizeFields(); - return obj; - } - - return new SmartPlainText(input); - } - catch - { - return new SmartPlainText(input); - } - } -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedAuthor.cs b/src/Ellie.Bot.Common/SmartText/SmartTextEmbedAuthor.cs deleted file mode 100644 index f89a437..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedAuthor.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie; - -public class SmartTextEmbedAuthor -{ - public string Name { get; set; } - - [JsonProperty("icon_url")] - public string IconUrl { get; set; } - - public string Url { get; set; } -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedField.cs b/src/Ellie.Bot.Common/SmartText/SmartTextEmbedField.cs deleted file mode 100644 index 338c084..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedField.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie; - -public class SmartTextEmbedField -{ - public string Name { get; set; } - public string Value { get; set; } - public bool Inline { get; set; } -} diff --git a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedFooter.cs b/src/Ellie.Bot.Common/SmartText/SmartTextEmbedFooter.cs deleted file mode 100644 index 6e3e6c6..0000000 --- a/src/Ellie.Bot.Common/SmartText/SmartTextEmbedFooter.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie; - -public class SmartTextEmbedFooter -{ - public string Text { get; set; } - - [JsonProperty("icon_url")] - public string IconUrl { get; set; } -} diff --git a/src/Ellie.Bot.Common/TypeReaderResult.cs b/src/Ellie.Bot.Common/TypeReaderResult.cs deleted file mode 100644 index f92f098..0000000 --- a/src/Ellie.Bot.Common/TypeReaderResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Ellie.Common.TypeReaders; - -public readonly struct TypeReaderResult -{ - public bool IsSuccess - => _result.IsSuccess; - - public IReadOnlyCollection Values - => _result.Values; - - private readonly Discord.Commands.TypeReaderResult _result; - - private TypeReaderResult(in Discord.Commands.TypeReaderResult result) - => _result = result; - - public static implicit operator TypeReaderResult(in Discord.Commands.TypeReaderResult result) - => new(result); - - public static implicit operator Discord.Commands.TypeReaderResult(in TypeReaderResult wrapper) - => wrapper._result; -} - -public static class TypeReaderResult -{ - public static TypeReaderResult FromError(CommandError error, string reason) - => Discord.Commands.TypeReaderResult.FromError(error, reason); - - public static TypeReaderResult FromSuccess(in T value) - => Discord.Commands.TypeReaderResult.FromSuccess(value); -} diff --git a/src/Ellie.Bot.Common/TypeReaders/CommandOrExprInfo.cs b/src/Ellie.Bot.Common/TypeReaders/CommandOrExprInfo.cs deleted file mode 100644 index 13d57cd..0000000 --- a/src/Ellie.Bot.Common/TypeReaders/CommandOrExprInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public class CommandOrExprInfo -{ - public enum Type - { - Normal, - Custom - } - - public string Name { get; set; } - public Type CmdType { get; set; } - - public bool IsCustom - => CmdType == Type.Custom; - - public CommandOrExprInfo(string input, Type type) - { - Name = input; - CmdType = type; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/TypeReaders/GuildDateTimeTypeReader.cs b/src/Ellie.Bot.Common/TypeReaders/GuildDateTimeTypeReader.cs deleted file mode 100644 index 9296043..0000000 --- a/src/Ellie.Bot.Common/TypeReaders/GuildDateTimeTypeReader.cs +++ /dev/null @@ -1,49 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class GuildDateTimeTypeReader : EllieTypeReader -{ - private readonly ITimezoneService _gts; - - public GuildDateTimeTypeReader(ITimezoneService gts) - => _gts = gts; - - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - var gdt = Parse(context.Guild.Id, input); - if (gdt is null) - { - return new(TypeReaderResult.FromError(CommandError.ParseFailed, - "Input string is in an incorrect format.")); - } - - return new(TypeReaderResult.FromSuccess(gdt)); - } - - private GuildDateTime Parse(ulong guildId, string input) - { - if (!DateTime.TryParse(input, out var dt)) - return null; - - var tz = _gts.GetTimeZoneOrUtc(guildId); - - return new(tz, dt); - } -} - -public class GuildDateTime -{ - public TimeZoneInfo Timezone { get; } - public DateTime CurrentGuildTime { get; } - public DateTime InputTime { get; } - public DateTime InputTimeUtc { get; } - - public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime) - { - var now = DateTime.UtcNow; - Timezone = guildTimezone; - CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone); - InputTime = inputTime; - InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc); - } -} diff --git a/src/Ellie.Bot.Common/Yml/CommentAttribute.cs b/src/Ellie.Bot.Common/Yml/CommentAttribute.cs deleted file mode 100644 index 478c02f..0000000 --- a/src/Ellie.Bot.Common/Yml/CommentAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Common.Yml; - -[AttributeUsage(AttributeTargets.Property)] -public class CommentAttribute : Attribute -{ - public string Comment { get; } - - public CommentAttribute(string comment) - => Comment = comment; -} diff --git a/src/Ellie.Bot.Common/Yml/CommentGatheringTypeInspector.cs b/src/Ellie.Bot.Common/Yml/CommentGatheringTypeInspector.cs deleted file mode 100644 index 8420b98..0000000 --- a/src/Ellie.Bot.Common/Yml/CommentGatheringTypeInspector.cs +++ /dev/null @@ -1,65 +0,0 @@ -#nullable disable -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.TypeInspectors; - -namespace Ellie.Common.Yml; - -public class CommentGatheringTypeInspector : TypeInspectorSkeleton -{ - public readonly ITypeInspector _innerTypeDescriptor; - - public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor) - => _innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor)); - - public override IEnumerable GetProperties(Type type, object container) - => _innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d)); - - private sealed class CommentsPropertyDescriptor : IPropertyDescriptor - { - public string Name { get; } - - public Type Type - => _baseDescriptor.Type; - - public Type TypeOverride - { - get => _baseDescriptor.TypeOverride; - set => _baseDescriptor.TypeOverride = value; - } - - public int Order { get; set; } - - public ScalarStyle ScalarStyle - { - get => _baseDescriptor.ScalarStyle; - set => _baseDescriptor.ScalarStyle = value; - } - - public bool CanWrite - => _baseDescriptor.CanWrite; - - private readonly IPropertyDescriptor _baseDescriptor; - - public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor) - { - _baseDescriptor = baseDescriptor; - Name = baseDescriptor.Name; - } - - public void Write(object target, object value) - => _baseDescriptor.Write(target, value); - - public T GetCustomAttribute() - where T : Attribute - => _baseDescriptor.GetCustomAttribute(); - - public IObjectDescriptor Read(object target) - { - var comment = _baseDescriptor.GetCustomAttribute(); - return comment is not null - ? new CommentsObjectDescriptor(_baseDescriptor.Read(target), comment.Comment) - : _baseDescriptor.Read(target); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Yml/CommentsObjectDescriptor.cs b/src/Ellie.Bot.Common/Yml/CommentsObjectDescriptor.cs deleted file mode 100644 index 08cddb7..0000000 --- a/src/Ellie.Bot.Common/Yml/CommentsObjectDescriptor.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable disable -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Ellie.Common.Yml; - -public sealed class CommentsObjectDescriptor : IObjectDescriptor -{ - public string Comment { get; } - - public object Value - => _innerDescriptor.Value; - - public Type Type - => _innerDescriptor.Type; - - public Type StaticType - => _innerDescriptor.StaticType; - - public ScalarStyle ScalarStyle - => _innerDescriptor.ScalarStyle; - - private readonly IObjectDescriptor _innerDescriptor; - - public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment) - { - _innerDescriptor = innerDescriptor; - Comment = comment; - } -} diff --git a/src/Ellie.Bot.Common/Yml/CommentsObjectGraphVisitor.cs b/src/Ellie.Bot.Common/Yml/CommentsObjectGraphVisitor.cs deleted file mode 100644 index c485549..0000000 --- a/src/Ellie.Bot.Common/Yml/CommentsObjectGraphVisitor.cs +++ /dev/null @@ -1,29 +0,0 @@ -#nullable disable -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.ObjectGraphVisitors; - -namespace Ellie.Common.Yml; - -public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor -{ - public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) - : base(nextVisitor) - { - } - - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) - { - if (value is CommentsObjectDescriptor commentsDescriptor - && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment)) - { - var parts = commentsDescriptor.Comment.Split('\n'); - - foreach (var part in parts) - context.Emit(new Comment(part.Trim(), false)); - } - - return base.EnterMapping(key, value, context); - } -} diff --git a/src/Ellie.Bot.Common/Yml/MultilineScalarFlowStyleEmitter.cs b/src/Ellie.Bot.Common/Yml/MultilineScalarFlowStyleEmitter.cs deleted file mode 100644 index 096ea2d..0000000 --- a/src/Ellie.Bot.Common/Yml/MultilineScalarFlowStyleEmitter.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable disable -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.EventEmitters; - -namespace Ellie.Common.Yml; - -public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter -{ - public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter) - : base(nextEmitter) - { - } - - public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) - { - if (typeof(string).IsAssignableFrom(eventInfo.Source.Type)) - { - var value = eventInfo.Source.Value as string; - if (!string.IsNullOrEmpty(value)) - { - var isMultiline = value.IndexOfAny(new[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0; - if (isMultiline) - { - eventInfo = new(eventInfo.Source) - { - Style = ScalarStyle.Literal - }; - } - } - } - - nextEmitter.Emit(eventInfo, emitter); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/Yml/Rgba32Converter.cs b/src/Ellie.Bot.Common/Yml/Rgba32Converter.cs deleted file mode 100644 index 477e8ec..0000000 --- a/src/Ellie.Bot.Common/Yml/Rgba32Converter.cs +++ /dev/null @@ -1,47 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp.PixelFormats; -using System.Globalization; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Ellie.Common.Yml; - -public class Rgba32Converter : IYamlTypeConverter -{ - public bool Accepts(Type type) - => type == typeof(Rgba32); - - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = Rgba32.ParseHex(scalar.Value); - return result; - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var color = (Rgba32)value; - var val = (uint)((color.B << 0) | (color.G << 8) | (color.R << 16)); - emitter.Emit(new Scalar(val.ToString("X6").ToLower())); - } -} - -public class CultureInfoConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) - => type == typeof(CultureInfo); - - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = new CultureInfo(scalar.Value); - return result; - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var ci = (CultureInfo)value; - emitter.Emit(new Scalar(ci.Name)); - } -} diff --git a/src/Ellie.Bot.Common/Yml/UrlConverter.cs b/src/Ellie.Bot.Common/Yml/UrlConverter.cs deleted file mode 100644 index 3054e65..0000000 --- a/src/Ellie.Bot.Common/Yml/UrlConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Ellie.Common.Yml; - -public class UriConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) - => type == typeof(Uri); - - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = new Uri(scalar.Value); - return result; - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var uri = (Uri)value; - emitter.Emit(new Scalar(uri.ToString())); - } -} diff --git a/src/Ellie.Bot.Common/Yml/Yaml.cs b/src/Ellie.Bot.Common/Yml/Yaml.cs deleted file mode 100644 index ef7e114..0000000 --- a/src/Ellie.Bot.Common/Yml/Yaml.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Ellie.Common.Yml; - -public class Yaml -{ - public static ISerializer Serializer - => new SerializerBuilder().WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) - .WithEmissionPhaseObjectGraphVisitor(args - => new CommentsObjectGraphVisitor(args.InnerVisitor)) - .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithIndentedSequences() - .WithTypeConverter(new Rgba32Converter()) - .WithTypeConverter(new CultureInfoConverter()) - .WithTypeConverter(new UriConverter()) - .Build(); - - public static IDeserializer Deserializer - => new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeConverter(new Rgba32Converter()) - .WithTypeConverter(new CultureInfoConverter()) - .WithTypeConverter(new UriConverter()) - .IgnoreUnmatchedProperties() - .Build(); -} diff --git a/src/Ellie.Bot.Common/_Extensions/BotCredentialsExtensions.cs b/src/Ellie.Bot.Common/_Extensions/BotCredentialsExtensions.cs deleted file mode 100644 index 3e3ad90..0000000 --- a/src/Ellie.Bot.Common/_Extensions/BotCredentialsExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ellie.Extensions; - -public static class BotCredentialsExtensions -{ - public static bool IsOwner(this IBotCredentials creds, IUser user) - => creds.IsOwner(user.Id); - - public static bool IsOwner(this IBotCredentials creds, ulong userId) - => creds.OwnerIds.Contains(userId); -} diff --git a/src/Ellie.Bot.Common/_Extensions/Extensions.cs b/src/Ellie.Bot.Common/_Extensions/Extensions.cs deleted file mode 100644 index 1452e4d..0000000 --- a/src/Ellie.Bot.Common/_Extensions/Extensions.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Humanizer.Localisation; -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; -using System.Text.RegularExpressions; -using Ellie.Marmalade; - -namespace Ellie.Extensions; - -public static class Extensions -{ - public static DateOnly ToDateOnly(this DateTime dateTime) - => DateOnly.FromDateTime(dateTime); - - public static bool IsBeforeToday(this DateTime date) - => date < DateTime.UtcNow.Date; - - private static readonly Regex _urlRegex = - new(@"^(https?|ftp)://(?[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled); - - public static IEmbedBuilder WithAuthor(this IEmbedBuilder eb, IUser author) - => eb.WithAuthor(author.ToString()!, author.RealAvatarUrl().ToString()); - - public static Task EditAsync(this IUserMessage msg, SmartText text) - => text switch - { - SmartEmbedText set => msg.ModifyAsync(x => - { - x.Embed = set.IsValid ? set.GetEmbed().Build() : null; - x.Content = set.PlainText?.SanitizeMentions() ?? ""; - }), - SmartEmbedTextArray set => msg.ModifyAsync(x => - { - x.Embeds = set.GetEmbedBuilders().Map(eb => eb.Build()); - x.Content = set.Content?.SanitizeMentions() ?? ""; - }), - SmartPlainText spt => msg.ModifyAsync(x => - { - x.Content = spt.Text.SanitizeMentions(); - x.Embed = null; - }), - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static ulong[] GetGuildIds(this DiscordSocketClient client) - => client.Guilds - .Map(x => x.Id); - - /// - /// Generates a string in the format HHH:mm if timespan is >= 2m. - /// Generates a string in the format 00:mm:ss if timespan is less than 2m. - /// - /// Timespan to convert to string - /// Formatted duration string - public static string ToPrettyStringHm(this TimeSpan span) - => span.Humanize(2, minUnit: TimeUnit.Second); - - public static bool TryGetUrlPath(this string input, out string path) - { - var match = _urlRegex.Match(input); - if (match.Success) - { - path = match.Groups["path"].Value; - return true; - } - - path = string.Empty; - return false; - } - - public static IEmote ToIEmote(this string emojiStr) - => Emote.TryParse(emojiStr, out var maybeEmote) ? maybeEmote : new Emoji(emojiStr); - - - /// - /// First 10 characters of teh bot token. - /// - public static string RedisKey(this IBotCredentials bc) - => bc.Token[..10]; - - public static bool IsAuthor(this IMessage msg, IDiscordClient client) - => msg.Author?.Id == client.CurrentUser.Id; - - public static string RealSummary( - this CommandInfo cmd, - IBotStrings strings, - IMarmaladeLoaderSevice marmalades, - CultureInfo culture, - string prefix) - { - string description; - if (cmd.Remarks?.StartsWith("marmalade///") ?? false) - { - // command method name is kept in Summary - // marmalade/// is kept in remarks - // this way I can find the name of the marmalade, and then name of the command for which - // the description should be loaded - var marmaladeName = cmd.Remarks.Split("///")[1]; - description = marmalades.GetCommandDescription(marmaladeName, cmd.Summary, culture); - } - else - { - description = strings.GetCommandStrings(cmd.Summary, culture).Desc; - } - - return string.Format(description, prefix); - } - - public static string[] RealRemarksArr( - this CommandInfo cmd, - IBotStrings strings, - IMarmaladeLoaderSevice marmalades, - CultureInfo culture, - string prefix) - { - string[] args; - if (cmd.Remarks?.StartsWith("marmalade///") ?? false) - { - // command method name is kept in Summary - // marmalade/// is kept in remarks - // this way I can find the name of the marmalade, - // and command for which data should be loaded - var marmaladeName = cmd.Remarks.Split("///")[1]; - args = marmalades.GetCommandExampleArgs(marmaladeName, cmd.Summary, culture); - } - else - { - args = strings.GetCommandStrings(cmd.Summary, culture).Args; - } - - return args.Map(arg => GetFullUsage(cmd.Aliases.First(), arg, prefix)); - } - - private static string GetFullUsage(string commandName, string args, string prefix) - => $"{prefix}{commandName} {string.Format(args, prefix)}".TrimEnd(); - - public static IEmbedBuilder AddPaginatedFooter(this IEmbedBuilder embed, int curPage, int? lastPage) - { - if (lastPage is not null) - return embed.WithFooter($"{curPage + 1} / {lastPage + 1}"); - return embed.WithFooter(curPage.ToString()); - } - - public static IEmbedBuilder WithOkColor(this IEmbedBuilder eb) - => eb.WithColor(EmbedColor.Ok); - - public static IEmbedBuilder WithPendingColor(this IEmbedBuilder eb) - => eb.WithColor(EmbedColor.Pending); - - public static IEmbedBuilder WithErrorColor(this IEmbedBuilder eb) - => eb.WithColor(EmbedColor.Error); - - public static IMessage DeleteAfter(this IUserMessage msg, float seconds, ILogCommandService? logService = null) - { - Task.Run(async () => - { - await Task.Delay((int)(seconds * 1000)); - if (logService is not null) - logService.AddDeleteIgnore(msg.Id); - - try - { - await msg.DeleteAsync(); - } - catch - { - } - }); - return msg; - } - - public static ModuleInfo GetTopLevelModule(this ModuleInfo module) - { - while (module.Parent is not null) - module = module.Parent; - - 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); - return users.Where(u => u.RoleIds.Contains(role.Id)); - } - - public static string ToJson(this T any, JsonSerializerOptions? options = null) - => JsonSerializer.Serialize(any, options); - - public static Stream ToStream(this IEnumerable bytes, bool canWrite = false) - { - var ms = new MemoryStream(bytes as byte[] ?? bytes.ToArray(), canWrite); - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - - public static IEnumerable GetRoles(this IGuildUser user) - => user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r is not null); - - public static void Lap(this Stopwatch sw, string checkpoint) - { - Log.Information("Checkpoint {CheckPoint}: {Time}ms", checkpoint, sw.Elapsed.TotalMilliseconds); - sw.Restart(); - } -} diff --git a/src/Ellie.Bot.Common/_Extensions/IMessageChannelExtensions.cs b/src/Ellie.Bot.Common/_Extensions/IMessageChannelExtensions.cs deleted file mode 100644 index 0598638..0000000 --- a/src/Ellie.Bot.Common/_Extensions/IMessageChannelExtensions.cs +++ /dev/null @@ -1,328 +0,0 @@ -namespace Ellie.Extensions; - -public static class MessageChannelExtensions -{ - // main overload that all other send methods reduce to - public static Task SendAsync( - this IMessageChannel channel, - string? plainText, - Embed? embed = null, - IReadOnlyCollection? embeds = null, - bool sanitizeAll = false, - MessageComponent? components = null) - { - plainText = sanitizeAll - ? plainText?.SanitizeAllMentions() ?? "" - : plainText?.SanitizeMentions() ?? ""; - - return channel.SendMessageAsync(plainText, - embed: embed, - embeds: embeds is null - ? null - : embeds as Embed[] ?? embeds.ToArray(), - components: components); - } - - public static async Task SendAsync( - this IMessageChannel channel, - string? plainText, - EllieInteraction? inter, - Embed? embed = null, - IReadOnlyCollection? embeds = null, - bool sanitizeAll = false) - { - var msg = await channel.SendAsync(plainText, - embed, - embeds, - sanitizeAll, - inter?.CreateComponent()); - - if (inter is not null) - await inter.RunAsync(msg); - - return msg; - } - - public static Task SendAsync( - this IMessageChannel channel, - SmartText text, - bool sanitizeAll = false) - => text switch - { - SmartEmbedText set => channel.SendAsync(set.PlainText, - set.IsValid ? set.GetEmbed().Build() : null, - sanitizeAll: sanitizeAll), - SmartPlainText st => channel.SendAsync(st.Text, - default(Embed), - sanitizeAll: sanitizeAll), - SmartEmbedTextArray arr => channel.SendAsync(arr.Content, - embeds: arr.GetEmbedBuilders().Map(e => e.Build())), - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static Task EmbedAsync( - this IMessageChannel ch, - IEmbedBuilder? embed, - string plainText = "", - IReadOnlyCollection? embeds = null, - EllieInteraction? inter = null) - => ch.SendAsync(plainText, - inter, - embed: embed?.Build(), - embeds: embeds?.Map(x => x.Build())); - - public static Task SendAsync( - this IMessageChannel ch, - IEmbedBuilderService eb, - string text, - MsgType type, - EllieInteraction? inter = null) - { - var builder = eb.Create().WithDescription(text); - - builder = (type switch - { - MsgType.Error => builder.WithErrorColor(), - MsgType.Ok => builder.WithOkColor(), - MsgType.Pending => builder.WithPendingColor(), - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }); - - return ch.EmbedAsync(builder, inter: inter); - } - - // regular send overloads - public static Task SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text) - => ch.SendAsync(eb, text, MsgType.Error); - - public static Task SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text) - => ch.SendAsync(eb, text, MsgType.Ok); - - public static Task SendAsync( - this IMessageChannel ch, - IEmbedBuilderService eb, - MsgType type, - string? title, - string text, - string? url = null, - string? footer = null) - { - var embed = eb.Create() - .WithDescription(text) - .WithTitle(title); - - if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) - embed.WithUrl(url); - - if (!string.IsNullOrWhiteSpace(footer)) - embed.WithFooter(footer); - - embed = type switch - { - MsgType.Error => embed.WithErrorColor(), - MsgType.Ok => embed.WithOkColor(), - MsgType.Pending => embed.WithPendingColor(), - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }; - - return ch.EmbedAsync(embed); - } - - // embed title and optional footer overloads - - public static Task SendConfirmAsync( - this IMessageChannel ch, - IEmbedBuilderService eb, - string? title, - string text, - string? url = null, - string? footer = null) - => ch.SendAsync(eb, MsgType.Ok, title, text, url, footer); - - public static Task SendErrorAsync( - this IMessageChannel ch, - IEmbedBuilderService eb, - string title, - string text, - string? url = null, - string? footer = null) - => ch.SendAsync(eb, MsgType.Error, title, text, url, footer); - - public static Task SendPaginatedConfirmAsync( - this ICommandContext ctx, - int currentPage, - Func pageFunc, - int totalElements, - int itemsPerPage, - bool addPaginatedFooter = true) - => ctx.SendPaginatedConfirmAsync(currentPage, - x => Task.FromResult(pageFunc(x)), - totalElements, - itemsPerPage, - addPaginatedFooter); - - private const string BUTTON_LEFT = "BUTTON_LEFT"; - private const string BUTTON_RIGHT = "BUTTON_RIGHT"; - - private static readonly IEmote _arrowLeft = Emote.Parse("<:x:969658061805465651>"); - private static readonly IEmote _arrowRight = Emote.Parse("<:x:969658062220701746>"); - - public static Task SendPaginatedConfirmAsync( - this ICommandContext ctx, - int currentPage, - Func> pageFunc, - int totalElements, - int itemsPerPage, - bool addPaginatedFooter = true) - => ctx.SendPaginatedConfirmAsync(currentPage, - pageFunc, - default(Func?>>), - totalElements, - itemsPerPage, - addPaginatedFooter); - - public static async Task SendPaginatedConfirmAsync( - this ICommandContext ctx, - int currentPage, - Func> pageFunc, - Func?>>? interFactory, - int totalElements, - int itemsPerPage, - bool addPaginatedFooter = true) - { - var lastPage = (totalElements - 1) / itemsPerPage; - - var embed = await pageFunc(currentPage); - - if (addPaginatedFooter) - embed.AddPaginatedFooter(currentPage, lastPage); - - SimpleInteraction? maybeInter = null; - async Task GetComponentBuilder() - { - var cb = new ComponentBuilder(); - - cb.WithButton(new ButtonBuilder() - .WithStyle(ButtonStyle.Primary) - .WithCustomId(BUTTON_LEFT) - .WithDisabled(lastPage == 0) - .WithEmote(_arrowLeft) - .WithDisabled(currentPage <= 0)); - - if (interFactory is not null) - { - maybeInter = await interFactory(currentPage); - - if (maybeInter is not null) - cb.WithButton(maybeInter.Button); - } - - cb.WithButton(new ButtonBuilder() - .WithStyle(ButtonStyle.Primary) - .WithCustomId(BUTTON_RIGHT) - .WithDisabled(lastPage == 0 || currentPage >= lastPage) - .WithEmote(_arrowRight)); - - return cb; - } - - async Task UpdatePageAsync(SocketMessageComponent smc) - { - var toSend = await pageFunc(currentPage); - if (addPaginatedFooter) - toSend.AddPaginatedFooter(currentPage, lastPage); - - var component = (await GetComponentBuilder()).Build(); - - await smc.ModifyOriginalResponseAsync(x => - { - x.Embed = toSend.Build(); - x.Components = component; - }); - } - - var component = (await GetComponentBuilder()).Build(); - var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component); - - async Task OnInteractionAsync(SocketInteraction si) - { - try - { - if (si is not SocketMessageComponent smc) - return; - - if (smc.Message.Id != msg.Id) - return; - - await si.DeferAsync(); - if (smc.User.Id != ctx.User.Id) - return; - - if (smc.Data.CustomId == BUTTON_LEFT) - { - if (currentPage == 0) - return; - - --currentPage; - _ = UpdatePageAsync(smc); - } - else if (smc.Data.CustomId == BUTTON_RIGHT) - { - if (currentPage >= lastPage) - return; - - ++currentPage; - _ = UpdatePageAsync(smc); - } - else if (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId) - { - await inter.TriggerAsync(smc); - _ = UpdatePageAsync(smc); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message); - } - } - - if (lastPage == 0 && interFactory is null) - return; - - var client = (DiscordSocketClient)ctx.Client; - - client.InteractionCreated += OnInteractionAsync; - - await Task.Delay(30_000); - - client.InteractionCreated -= OnInteractionAsync; - - await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build()); - } - - private static readonly Emoji _okEmoji = new Emoji("✅"); - private static readonly Emoji _warnEmoji = new Emoji("⚠️"); - private static readonly Emoji _errorEmoji = new Emoji("❌"); - - public static Task ReactAsync(this ICommandContext ctx, MsgType type) - { - var emoji = type switch - { - MsgType.Error => _errorEmoji, - MsgType.Pending => _warnEmoji, - MsgType.Ok => _okEmoji, - _ => throw new ArgumentOutOfRangeException(nameof(type)), - }; - - return ctx.Message.AddReactionAsync(emoji); - } - - public static Task OkAsync(this ICommandContext ctx) - => ctx.ReactAsync(MsgType.Ok); - - public static Task ErrorAsync(this ICommandContext ctx) - => ctx.ReactAsync(MsgType.Error); - - public static Task WarningAsync(this ICommandContext ctx) - => ctx.ReactAsync(MsgType.Pending); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_Extensions/LinkedListExtensions.cs b/src/Ellie.Bot.Common/_Extensions/LinkedListExtensions.cs deleted file mode 100644 index a2cc826..0000000 --- a/src/Ellie.Bot.Common/_Extensions/LinkedListExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Ellie.Extensions; - -public static class LinkedListExtensions -{ - public static LinkedListNode? FindNode(this LinkedList list, Func predicate) - { - var node = list.First; - while (node is not null) - { - if (predicate(node.Value)) - return node; - - node = node.Next; - } - - return null; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_Extensions/NumberExtensions.cs b/src/Ellie.Bot.Common/_Extensions/NumberExtensions.cs deleted file mode 100644 index a65fc5f..0000000 --- a/src/Ellie.Bot.Common/_Extensions/NumberExtensions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Extensions; - -public static class NumberExtensions -{ - public static DateTimeOffset ToUnixTimestamp(this double number) - => new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number); -} diff --git a/src/Ellie.Bot.Common/_Extensions/ReflectionExtensions.cs b/src/Ellie.Bot.Common/_Extensions/ReflectionExtensions.cs deleted file mode 100644 index c66df67..0000000 --- a/src/Ellie.Bot.Common/_Extensions/ReflectionExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Ellie.Extensions; - -public static class ReflectionExtensions -{ - public static bool IsAssignableToGenericType(this Type givenType, Type genericType) - { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) - { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - return true; - } - - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) - return true; - - var baseType = givenType.BaseType; - if (baseType == null) return false; - - return IsAssignableToGenericType(baseType, genericType); - } -} diff --git a/src/Ellie.Bot.Common/_Extensions/Rgba32Extensions.cs b/src/Ellie.Bot.Common/_Extensions/Rgba32Extensions.cs deleted file mode 100644 index d521221..0000000 --- a/src/Ellie.Bot.Common/_Extensions/Rgba32Extensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace Ellie.Extensions; - -public static class Rgba32Extensions -{ - public static Image Merge(this IEnumerable> images) - => images.Merge(out _); - - public static Image Merge(this IEnumerable> images, out IImageFormat format) - { - format = PngFormat.Instance; - - void DrawFrame(IList> imgArray, Image imgFrame, int frameNumber) - { - var xOffset = 0; - for (var i = 0; i < imgArray.Count; i++) - { - using var frame = imgArray[i].Frames.CloneFrame(frameNumber % imgArray[i].Frames.Count); - var offset = xOffset; - imgFrame.Mutate(x => x.DrawImage(frame, new(offset, 0), new GraphicsOptions())); - xOffset += imgArray[i].Bounds().Width; - } - } - - var imgs = images.ToList(); - var frames = imgs.Max(x => x.Frames.Count); - - var width = imgs.Sum(img => img.Width); - var height = imgs.Max(img => img.Height); - var canvas = new Image(width, height); - if (frames == 1) - { - DrawFrame(imgs, canvas, 0); - return canvas; - } - - format = GifFormat.Instance; - for (var j = 0; j < frames; j++) - { - using var imgFrame = new Image(width, height); - DrawFrame(imgs, imgFrame, j); - - var frameToAdd = imgFrame.Frames[0]; - frameToAdd.Metadata.GetGifMetadata().DisposalMethod = GifDisposalMethod.RestoreToBackground; - canvas.Frames.AddFrame(frameToAdd); - } - - canvas.Frames.RemoveFrame(0); - return canvas; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_Extensions/SocketMessageComponentExtensions.cs b/src/Ellie.Bot.Common/_Extensions/SocketMessageComponentExtensions.cs deleted file mode 100644 index 87b53ff..0000000 --- a/src/Ellie.Bot.Common/_Extensions/SocketMessageComponentExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Ellie.Extensions; - -public static class SocketMessageComponentExtensions -{ - public static Task RespondAsync( - this SocketMessageComponent smc, - string? plainText, - Embed? embed = null, - IReadOnlyCollection? embeds = null, - bool sanitizeAll = false, - MessageComponent? components = null, - bool ephemeral = true) - { - plainText = sanitizeAll - ? plainText?.SanitizeAllMentions() ?? "" - : plainText?.SanitizeMentions() ?? ""; - - return smc.RespondAsync(plainText, - embed: embed, - embeds: embeds is null - ? null - : embeds as Embed[] ?? embeds.ToArray(), - components: components, - ephemeral: ephemeral); - } - - public static Task RespondAsync( - this SocketMessageComponent smc, - SmartText text, - bool sanitizeAll = false, - bool ephemeral = true) - => text switch - { - SmartEmbedText set => smc.RespondAsync(set.PlainText, - set.IsValid ? set.GetEmbed().Build() : null, - sanitizeAll: sanitizeAll, - ephemeral: ephemeral), - SmartPlainText st => smc.RespondAsync(st.Text, - default(Embed), - sanitizeAll: sanitizeAll, - ephemeral: ephemeral), - SmartEmbedTextArray arr => smc.RespondAsync(arr.Content, - embeds: arr.GetEmbedBuilders().Map(e => e.Build()), - ephemeral: ephemeral), - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static Task EmbedAsync( - this SocketMessageComponent smc, - IEmbedBuilder? embed, - string plainText = "", - IReadOnlyCollection? embeds = null, - EllieInteraction? inter = null, - bool ephemeral = false) - => smc.RespondAsync(plainText, - embed: embed?.Build(), - embeds: embeds?.Map(x => x.Build()), - ephemeral: ephemeral); - - public static Task RespondAsync( - this SocketMessageComponent ch, - IEmbedBuilderService eb, - string text, - MsgType type, - bool ephemeral = false, - EllieInteraction? inter = null) - { - var builder = eb.Create().WithDescription(text); - - builder = (type switch - { - MsgType.Error => builder.WithErrorColor(), - MsgType.Ok => builder.WithOkColor(), - MsgType.Pending => builder.WithPendingColor(), - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }); - - return ch.EmbedAsync(builder, inter: inter, ephemeral: ephemeral); - } - - // embed title and optional footer overloads - - public static Task RespondErrorAsync( - this SocketMessageComponent smc, - IEmbedBuilderService eb, - string text, - bool ephemeral = false) - => smc.RespondAsync(eb, text, MsgType.Error, ephemeral); - - public static Task RespondConfirmAsync( - this SocketMessageComponent smc, - IEmbedBuilderService eb, - string text, - bool ephemeral = false) - => smc.RespondAsync(eb, text, MsgType.Ok, ephemeral); -} diff --git a/src/Ellie.Bot.Common/_Extensions/UserExtensions.cs b/src/Ellie.Bot.Common/_Extensions/UserExtensions.cs deleted file mode 100644 index 01c6e7e..0000000 --- a/src/Ellie.Bot.Common/_Extensions/UserExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Ellie.Db.Models; - -namespace Ellie.Extensions; - -public static class UserExtensions -{ - public static async Task EmbedAsync(this IUser user, IEmbedBuilder embed, string msg = "") - { - var ch = await user.CreateDMChannelAsync(); - return await ch.EmbedAsync(embed, msg); - } - - public static async Task SendAsync(this IUser user, SmartText text, bool sanitizeAll = false) - { - var ch = await user.CreateDMChannelAsync(); - return await ch.SendAsync(text, sanitizeAll); - } - - public static async Task SendConfirmAsync(this IUser user, IEmbedBuilderService eb, string text) - => await user.SendMessageAsync("", embed: eb.Create().WithOkColor().WithDescription(text).Build()); - - public static async Task SendErrorAsync(this IUser user, IEmbedBuilderService eb, string error) - => await user.SendMessageAsync("", embed: eb.Create().WithErrorColor().WithDescription(error).Build()); - - public static async Task SendPendingAsync(this IUser user, IEmbedBuilderService eb, string message) - => await user.SendMessageAsync("", embed: eb.Create().WithPendingColor().WithDescription(message).Build()); - - // This method is used by everything that fetches the avatar from a user - public static Uri RealAvatarUrl(this IUser usr, ushort size = 256) - => usr.AvatarId is null ? new(usr.GetDefaultAvatarUrl()) : new Uri(usr.GetAvatarUrl(ImageFormat.Auto, size)); - - // This method is only used for the xp card - public static Uri RealAvatarUrl(this DiscordUser usr) - => usr.AvatarId is null - ? new(CDN.GetDefaultUserAvatarUrl(ushort.Parse(usr.Discriminator))) - : new Uri(usr.AvatarId.StartsWith("a_", StringComparison.InvariantCulture) - ? $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.gif" - : $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.png"); -} diff --git a/src/Ellie.Bot.Common/_common/AddRemove.cs b/src/Ellie.Bot.Common/_common/AddRemove.cs deleted file mode 100644 index 4d1dd30..0000000 --- a/src/Ellie.Bot.Common/_common/AddRemove.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public enum AddRemove -{ - Add = int.MinValue, - Remove = int.MinValue + 1, - Rem = int.MinValue + 1, - Rm = int.MinValue + 1 -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/CmdStrings.cs b/src/Ellie.Bot.Common/_common/CmdStrings.cs deleted file mode 100644 index a2fef95..0000000 --- a/src/Ellie.Bot.Common/_common/CmdStrings.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Common; - -public class CmdStrings -{ - public string[] Usages { get; } - public string Description { get; } - - [JsonConstructor] - public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description) - { - Usages = usages; - Description = description; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/CommandData.cs b/src/Ellie.Bot.Common/_common/CommandData.cs deleted file mode 100644 index a1dbe8d..0000000 --- a/src/Ellie.Bot.Common/_common/CommandData.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public class CommandData -{ - public string Cmd { get; set; } - public string Desc { get; set; } - public string[] Usage { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/DownloadTracker.cs b/src/Ellie.Bot.Common/_common/DownloadTracker.cs deleted file mode 100644 index 521871b..0000000 --- a/src/Ellie.Bot.Common/_common/DownloadTracker.cs +++ /dev/null @@ -1,38 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public class DownloadTracker : IEService -{ - private ConcurrentDictionary LastDownloads { get; } = new(); - private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1); - - /// - /// Ensures all users on the specified guild were downloaded within the last hour. - /// - /// Guild to check and potentially download users from - /// Task representing download state - public async Task EnsureUsersDownloadedAsync(IGuild guild) - { -#if GLOBAL_ELLIE - return; -#endif - await _downloadUsersSemaphore.WaitAsync(); - try - { - var now = DateTime.UtcNow; - - // download once per hour at most - var added = LastDownloads.AddOrUpdate(guild.Id, - now, - (_, old) => now - old > TimeSpan.FromHours(1) ? now : old); - - // means that this entry was just added - download the users - if (added == now) - await guild.DownloadUsersAsync(); - } - finally - { - _downloadUsersSemaphore.Release(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/Helpers.cs b/src/Ellie.Bot.Common/_common/Helpers.cs deleted file mode 100644 index 797484d..0000000 --- a/src/Ellie.Bot.Common/_common/Helpers.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public static class Helpers -{ - public static void ReadErrorAndExit(int exitCode) - { - if (!Console.IsInputRedirected) - Console.ReadKey(); - - Environment.Exit(exitCode); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/ImageUrls.cs b/src/Ellie.Bot.Common/_common/ImageUrls.cs deleted file mode 100644 index 402f11b..0000000 --- a/src/Ellie.Bot.Common/_common/ImageUrls.cs +++ /dev/null @@ -1,51 +0,0 @@ -#nullable disable -using Ellie.Common.Yml; -using Cloneable; - -namespace Ellie.Common; - -[Cloneable] -public partial class ImageUrls : ICloneable -{ - [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 3; - - public CoinData Coins { get; set; } - public Uri[] Currency { get; set; } - public Uri[] Dice { get; set; } - public RategirlData Rategirl { get; set; } - public XpData Xp { get; set; } - - //new - public RipData Rip { get; set; } - public SlotData Slots { get; set; } - - public class RipData - { - public Uri Bg { get; set; } - public Uri Overlay { get; set; } - } - - public class SlotData - { - public Uri[] Emojis { get; set; } - public Uri Bg { get; set; } - } - - public class CoinData - { - public Uri[] Heads { get; set; } - public Uri[] Tails { get; set; } - } - - public class RategirlData - { - public Uri Matrix { get; set; } - public Uri Dot { get; set; } - } - - public class XpData - { - public Uri Bg { get; set; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/JsonConverters/CultureInfoConverter.cs b/src/Ellie.Bot.Common/_common/JsonConverters/CultureInfoConverter.cs deleted file mode 100644 index cda7dfe..0000000 --- a/src/Ellie.Bot.Common/_common/JsonConverters/CultureInfoConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Ellie.Common.JsonConverters; - -public class CultureInfoConverter : JsonConverter -{ - public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => new(reader.GetString() ?? "en-US"); - - public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options) - => writer.WriteStringValue(value.Name); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/JsonConverters/Rgba32Converter.cs b/src/Ellie.Bot.Common/_common/JsonConverters/Rgba32Converter.cs deleted file mode 100644 index 959a0e6..0000000 --- a/src/Ellie.Bot.Common/_common/JsonConverters/Rgba32Converter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using SixLabors.ImageSharp.PixelFormats; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Ellie.Common.JsonConverters; - -public class Rgba32Converter : JsonConverter -{ - public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => Rgba32.ParseHex(reader.GetString()); - - public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToHex()); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/LbOpts.cs b/src/Ellie.Bot.Common/_common/LbOpts.cs deleted file mode 100644 index dca77cd..0000000 --- a/src/Ellie.Bot.Common/_common/LbOpts.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -using CommandLine; - -namespace Ellie.Common; - -public class LbOpts : IEllieCommandOptions -{ - [Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")] - public bool Clean { get; set; } - - public void NormalizeOptions() - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/Linq2DbExpressions.cs b/src/Ellie.Bot.Common/_common/Linq2DbExpressions.cs deleted file mode 100644 index d066440..0000000 --- a/src/Ellie.Bot.Common/_common/Linq2DbExpressions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -using LinqToDB; -using System.Linq.Expressions; - -namespace Ellie.Common; - -public static class Linq2DbExpressions -{ - [ExpressionMethod(nameof(GuildOnShardExpression))] - public static bool GuildOnShard(ulong guildId, int totalShards, int shardId) - => throw new NotSupportedException(); - - private static Expression> GuildOnShardExpression() - => (guildId, totalShards, shardId) - => guildId / 4194304 % (ulong)totalShards == (ulong)shardId; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/LoginErrorHandler.cs b/src/Ellie.Bot.Common/_common/LoginErrorHandler.cs deleted file mode 100644 index 8aafc38..0000000 --- a/src/Ellie.Bot.Common/_common/LoginErrorHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -#nullable disable -using System.Net; -using System.Runtime.CompilerServices; - -namespace Ellie.Common; - -public class LoginErrorHandler -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Handle(Exception ex) - => Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Handle(HttpException ex) - { - switch (ex.HttpCode) - { - case HttpStatusCode.Unauthorized: - Log.Error("Your bot token is wrong.\n" - + "You can find the bot token under the Bot tab in the developer page.\n" - + "Fix your token in the credentials file and restart the bot"); - break; - - case HttpStatusCode.BadRequest: - Log.Error("Something has been incorrectly formatted in your credentials file.\n" - + "Use the JSON Guide as reference to fix it and restart the bot"); - Log.Error("If you are on Linux, make sure Redis is installed and running"); - break; - - case HttpStatusCode.RequestTimeout: - Log.Error("The request timed out. Make sure you have no external program blocking the bot " - + "from connecting to the internet"); - break; - - case HttpStatusCode.ServiceUnavailable: - case HttpStatusCode.InternalServerError: - Log.Error("Discord is having internal issues. Please, try again later"); - break; - - case HttpStatusCode.TooManyRequests: - Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" - + "Global ratelimits usually last for an hour"); - break; - - default: - Log.Warning("An error occurred while attempting to connect to Discord"); - break; - } - - Log.Fatal(ex, "Fatal error occurred while loading credentials"); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/OldCreds.cs b/src/Ellie.Bot.Common/_common/OldCreds.cs deleted file mode 100644 index 519d10e..0000000 --- a/src/Ellie.Bot.Common/_common/OldCreds.cs +++ /dev/null @@ -1,46 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public class OldCreds -{ - public string Token { get; set; } = string.Empty; - public ulong[] OwnerIds { get; set; } = new ulong[1]; - public string LoLApiKey { get; set; } = string.Empty; - public string GoogleApiKey { get; set; } = string.Empty; - public string MashapeKey { get; set; } = string.Empty; - public string OsuApiKey { get; set; } = string.Empty; - public string SoundCloudClientId { get; set; } = string.Empty; - public string CleverbotApiKey { get; set; } = string.Empty; - public string CarbonKey { get; set; } = string.Empty; - public int TotalShards { get; set; } = 1; - public string PatreonAccessToken { get; set; } = string.Empty; - public string PatreonCampaignId { get; set; } = "334038"; - public RestartConfig RestartCommand { get; set; } - - public string ShardRunCommand { get; set; } = string.Empty; - public string ShardRunArguments { get; set; } = string.Empty; - public int? ShardRunPort { get; set; } - public string MiningProxyUrl { get; set; } = string.Empty; - public string MiningProxyCreds { get; set; } = string.Empty; - - public string BotListToken { get; set; } = string.Empty; - public string TwitchClientId { get; set; } = string.Empty; - public string VotesToken { get; set; } = string.Empty; - public string VotesUrl { get; set; } = string.Empty; - public string RedisOptions { get; set; } = string.Empty; - public string LocationIqApiKey { get; set; } = string.Empty; - public string TimezoneDbApiKey { get; set; } = string.Empty; - public string CoinmarketcapApiKey { get; set; } = string.Empty; - - public class RestartConfig - { - public string Cmd { get; set; } - public string Args { get; set; } - - public RestartConfig(string cmd, string args) - { - Cmd = cmd; - Args = args; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/OptionsParser.cs b/src/Ellie.Bot.Common/_common/OptionsParser.cs deleted file mode 100644 index e4263f3..0000000 --- a/src/Ellie.Bot.Common/_common/OptionsParser.cs +++ /dev/null @@ -1,23 +0,0 @@ -using CommandLine; - -namespace Ellie.Common; - -public static class OptionsParser -{ - public static T ParseFrom(string[]? args) - where T : IEllieCommandOptions, new() - => ParseFrom(new T(), args).Item1; - - public static (T, bool) ParseFrom(T options, string[]? args) - where T : IEllieCommandOptions - { - using var p = new Parser(x => - { - x.HelpWriter = null; - }); - var res = p.ParseArguments(args); - var output = res.MapResult(x => x, _ => options); - output.NormalizeOptions(); - return (output, res.Tag == ParserResultType.Parsed); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/OsuMapData.cs b/src/Ellie.Bot.Common/_common/OsuMapData.cs deleted file mode 100644 index ebf9367..0000000 --- a/src/Ellie.Bot.Common/_common/OsuMapData.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Common; - -public class OsuMapData -{ - public string Title { get; set; } - public string Artist { get; set; } - public string Version { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/OsuUserBets.cs b/src/Ellie.Bot.Common/_common/OsuUserBets.cs deleted file mode 100644 index 67898fc..0000000 --- a/src/Ellie.Bot.Common/_common/OsuUserBets.cs +++ /dev/null @@ -1,58 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Common; - -public class OsuUserBests -{ - [JsonProperty("beatmap_id")] - public string BeatmapId { get; set; } - - [JsonProperty("score_id")] - public string ScoreId { get; set; } - - [JsonProperty("score")] - public string Score { get; set; } - - [JsonProperty("maxcombo")] - public string Maxcombo { get; set; } - - [JsonProperty("count50")] - public double Count50 { get; set; } - - [JsonProperty("count100")] - public double Count100 { get; set; } - - [JsonProperty("count300")] - public double Count300 { get; set; } - - [JsonProperty("countmiss")] - public int Countmiss { get; set; } - - [JsonProperty("countkatu")] - public double Countkatu { get; set; } - - [JsonProperty("countgeki")] - public double Countgeki { get; set; } - - [JsonProperty("perfect")] - public string Perfect { get; set; } - - [JsonProperty("enabled_mods")] - public int EnabledMods { get; set; } - - [JsonProperty("user_id")] - public string UserId { get; set; } - - [JsonProperty("date")] - public string Date { get; set; } - - [JsonProperty("rank")] - public string Rank { get; set; } - - [JsonProperty("pp")] - public double Pp { get; set; } - - [JsonProperty("replay_available")] - public string ReplayAvailable { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/Pokemon/PokemonNameId.cs b/src/Ellie.Bot.Common/_common/Pokemon/PokemonNameId.cs deleted file mode 100644 index 13e8360..0000000 --- a/src/Ellie.Bot.Common/_common/Pokemon/PokemonNameId.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Common.Pokemon; - -public class PokemonNameId -{ - public int Id { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemon.cs b/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemon.cs deleted file mode 100644 index eb861b3..0000000 --- a/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemon.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable disable - -using System.Text.Json.Serialization; - -namespace Ellie.Common.Pokemon; - -public class SearchPokemon -{ - [JsonPropertyName("num")] - public int Id { get; set; } - - public string Species { get; set; } - public string[] Types { get; set; } - public GenderRatioClass GenderRatio { get; set; } - public BaseStatsClass BaseStats { get; set; } - public Dictionary Abilities { get; set; } - public float HeightM { get; set; } - public float WeightKg { get; set; } - public string Color { get; set; } - public string[] Evos { get; set; } - public string[] EggGroups { get; set; } - - public class GenderRatioClass - { - public float M { get; set; } - public float F { get; set; } - } - - public class BaseStatsClass - { - public int Hp { get; set; } - public int Atk { get; set; } - public int Def { get; set; } - public int Spa { get; set; } - public int Spd { get; set; } - public int Spe { get; set; } - - public override string ToString() - => $@"💚**HP:** {Hp,-4} ⚔**ATK:** {Atk,-4} 🛡**DEF:** {Def,-4} -✨**SPA:** {Spa,-4} 🎇**SPD:** {Spd,-4} 💨**SPE:** {Spe,-4}"; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemonAbility.cs b/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemonAbility.cs deleted file mode 100644 index c0d79be..0000000 --- a/src/Ellie.Bot.Common/_common/Pokemon/SearchPokemonAbility.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Common.Pokemon; - -public class SearchPokemonAbility -{ - public string Desc { get; set; } - public string ShortDesc { get; set; } - public string Name { get; set; } - public float Rating { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/RequireObjectPropertiesContractResolver.cs b/src/Ellie.Bot.Common/_common/RequireObjectPropertiesContractResolver.cs deleted file mode 100644 index e187198..0000000 --- a/src/Ellie.Bot.Common/_common/RequireObjectPropertiesContractResolver.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Ellie.Common; - -public class RequireObjectPropertiesContractResolver : DefaultContractResolver -{ - protected override JsonObjectContract CreateObjectContract(Type objectType) - { - var contract = base.CreateObjectContract(objectType); - contract.ItemRequired = Required.DisallowNull; - return contract; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TriviaQuestionModel.cs b/src/Ellie.Bot.Common/_common/TriviaQuestionModel.cs deleted file mode 100644 index 3568706..0000000 --- a/src/Ellie.Bot.Common/_common/TriviaQuestionModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Common.Trivia; - -public sealed class TriviaQuestionModel -{ - public string Category { get; init; } - public string Question { get; init; } - public string ImageUrl { get; init; } - public string AnswerImageUrl { get; init; } - public string Answer { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/EmoteTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/EmoteTypeReader.cs deleted file mode 100644 index 0df3344..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/EmoteTypeReader.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class EmoteTypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext ctx, string input) - { - if (!Emote.TryParse(input, out var emote)) - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote")); - - return new(TypeReaderResult.FromSuccess(emote)); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/GuildTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/GuildTypeReader.cs deleted file mode 100644 index deafde2..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/GuildTypeReader.cs +++ /dev/null @@ -1,24 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class GuildTypeReader : EllieTypeReader -{ - private readonly DiscordSocketClient _client; - - public GuildTypeReader(DiscordSocketClient client) - => _client = client; - - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - input = input.Trim().ToUpperInvariant(); - var guilds = _client.Guilds; - IGuild guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) //by id - ?? guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name - - if (guild is not null) - return new(TypeReaderResult.FromSuccess(guild)); - - return new( - TypeReaderResult.FromError(CommandError.ParseFailed, "No guild by that name or Id found")); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/GuildUserTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/GuildUserTypeReader.cs deleted file mode 100644 index 2dcf5ed..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/GuildUserTypeReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Ellie.Common.TypeReaders; - -public sealed class GuildUserTypeReader : EllieTypeReader -{ - public override async ValueTask> ReadAsync(ICommandContext ctx, string input) - { - if (ctx.Guild is null) - return TypeReaderResult.FromError(CommandError.Unsuccessful, "Must be in a guild."); - - input = input.Trim(); - IGuildUser? user = null; - if (MentionUtils.TryParseUser(input, out var id)) - user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload); - - if (ulong.TryParse(input, out id)) - user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload); - - if (user is null) - { - var users = await ctx.Guild.GetUsersAsync(CacheMode.CacheOnly); - user = users.FirstOrDefault(x => x.Username == input) - ?? users.FirstOrDefault(x => - string.Equals(x.ToString(), input, StringComparison.InvariantCultureIgnoreCase)) - ?? users.FirstOrDefault(x => - string.Equals(x.Username, input, StringComparison.InvariantCultureIgnoreCase)); - } - - if (user is null) - return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); - - return TypeReaderResult.FromSuccess(user); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/KwumTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/KwumTypeReader.cs deleted file mode 100644 index 6ddab55..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/KwumTypeReader.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class KwumTypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - if (kwum.TryParse(input, out var val)) - return new(TypeReaderResult.FromSuccess(val)); - - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid kwum")); - } -} - -public sealed class SmartTextTypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext ctx, string input) - => new(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input))); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/Models/PermissionAction.cs b/src/Ellie.Bot.Common/_common/TypeReaders/Models/PermissionAction.cs deleted file mode 100644 index 22736ed..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/Models/PermissionAction.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders.Models; - -public class PermissionAction -{ - public static PermissionAction Enable - => new(true); - - public static PermissionAction Disable - => new(false); - - public bool Value { get; } - - public PermissionAction(bool value) - => Value = value; - - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - return false; - - return Value == ((PermissionAction)obj).Value; - } - - public override int GetHashCode() - => Value.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/Models/StoopidTime.cs b/src/Ellie.Bot.Common/_common/TypeReaders/Models/StoopidTime.cs deleted file mode 100644 index 555fe64..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/Models/StoopidTime.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace Ellie.Common.TypeReaders.Models; - -public class StoopidTime -{ - private static readonly Regex _regex = new( - @"^(?:(?\d)mo)?(?:(?\d{1,2})w)?(?:(?\d{1,2})d)?(?:(?\d{1,4})h)?(?:(?\d{1,5})m)?(?:(?\d{1,6})s)?$", - RegexOptions.Compiled | RegexOptions.Multiline); - - public string Input { get; set; } - public TimeSpan Time { get; set; } - - private StoopidTime() { } - - public static StoopidTime FromInput(string input) - { - var m = _regex.Match(input); - - if (m.Length == 0) - throw new ArgumentException("Invalid string input format."); - - var namesAndValues = new Dictionary(); - - foreach (var groupName in _regex.GetGroupNames()) - { - if (groupName == "0") - continue; - if (!int.TryParse(m.Groups[groupName].Value, out var value)) - { - namesAndValues[groupName] = 0; - continue; - } - - if (value < 1) - throw new ArgumentException($"Invalid {groupName} value."); - - namesAndValues[groupName] = value; - } - - var ts = new TimeSpan((30 * namesAndValues["months"]) + (7 * namesAndValues["weeks"]) + namesAndValues["days"], - namesAndValues["hours"], - namesAndValues["minutes"], - namesAndValues["seconds"]); - if (ts > TimeSpan.FromDays(90)) - throw new ArgumentException("Time is too long."); - - return new() - { - Input = input, - Time = ts - }; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/ModuleTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/ModuleTypeReader.cs deleted file mode 100644 index 74de6e3..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/ModuleTypeReader.cs +++ /dev/null @@ -1,50 +0,0 @@ -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class ModuleTypeReader : EllieTypeReader -{ - private readonly CommandService _cmds; - - public ModuleTypeReader(CommandService cmds) - => _cmds = cmds; - - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()) - .FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input) - ?.Key; - if (module is null) - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); - - return new(TypeReaderResult.FromSuccess(module)); - } -} - -public sealed class ModuleOrCrTypeReader : EllieTypeReader -{ - private readonly CommandService _cmds; - - public ModuleOrCrTypeReader(CommandService cmds) - => _cmds = cmds; - - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()) - .FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input) - ?.Key; - if (module is null && input != "ACTUALEXPRESSIONS") - return new(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); - - return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo - { - Name = input - })); - } -} - -public sealed class ModuleOrCrInfo -{ - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/PermissionActionTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/PermissionActionTypeReader.cs deleted file mode 100644 index 27a0111..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/PermissionActionTypeReader.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders.Models; - -namespace Ellie.Common.TypeReaders; - -/// -/// Used instead of bool for more flexible keywords for true/false only in the permission module -/// -public sealed class PermissionActionTypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - switch (input) - { - case "1": - case "T": - case "TRUE": - case "ENABLE": - case "ENABLED": - case "ALLOW": - case "PERMIT": - case "UNBAN": - return new(TypeReaderResult.FromSuccess(PermissionAction.Enable)); - case "0": - case "F": - case "FALSE": - case "DENY": - case "DISABLE": - case "DISABLED": - case "DISALLOW": - case "BAN": - return new(TypeReaderResult.FromSuccess(PermissionAction.Disable)); - default: - return new(TypeReaderResult.FromError(CommandError.ParseFailed, - "Did not receive a valid boolean value")); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/Rgba32TypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/Rgba32TypeReader.cs deleted file mode 100644 index 899586d..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/Rgba32TypeReader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Color = SixLabors.ImageSharp.Color; - -#nullable disable -namespace Ellie.Common.TypeReaders; - -public sealed class Rgba32TypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - input = input.Replace("#", "", StringComparison.InvariantCulture); - try - { - return ValueTask.FromResult(TypeReaderResult.FromSuccess(Color.ParseHex(input))); - } - catch - { - return ValueTask.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex.")); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Common/_common/TypeReaders/StoopidTimeTypeReader.cs b/src/Ellie.Bot.Common/_common/TypeReaders/StoopidTimeTypeReader.cs deleted file mode 100644 index feb4f84..0000000 --- a/src/Ellie.Bot.Common/_common/TypeReaders/StoopidTimeTypeReader.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders.Models; - -namespace Ellie.Common.TypeReaders; - -public sealed class StoopidTimeTypeReader : EllieTypeReader -{ - public override ValueTask> ReadAsync(ICommandContext context, string input) - { - if (string.IsNullOrWhiteSpace(input)) - return new(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty.")); - try - { - var time = StoopidTime.FromInput(input); - return new(TypeReaderResult.FromSuccess(time)); - } - catch (Exception ex) - { - return new(TypeReaderResult.FromError(CommandError.Exception, ex.Message)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/ClubExtensions.cs b/src/Ellie.Bot.Db/Extensions/ClubExtensions.cs deleted file mode 100644 index 0e5a419..0000000 --- a/src/Ellie.Bot.Db/Extensions/ClubExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db.Models; - -namespace Ellie.Db; - -public static class ClubExtensions -{ - private static IQueryable Include(this DbSet clubs) - => clubs.Include(x => x.Owner) - .Include(x => x.Applicants) - .ThenInclude(x => x.User) - .Include(x => x.Bans) - .ThenInclude(x => x.User) - .Include(x => x.Members) - .AsQueryable(); - - public static ClubInfo GetByOwner(this DbSet clubs, ulong userId) - => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId); - - public static ClubInfo GetByOwnerOrAdmin(this DbSet clubs, ulong userId) - => Include(clubs) - .FirstOrDefault(c => c.Owner.UserId == userId || c.Members.Any(u => u.UserId == userId && u.IsClubAdmin)); - - public static ClubInfo GetByMember(this DbSet clubs, ulong userId) - => Include(clubs).FirstOrDefault(c => c.Members.Any(u => u.UserId == userId)); - - public static ClubInfo GetByName(this DbSet clubs, string name) - => Include(clubs) - .FirstOrDefault(c => c.Name == name); - - public static List GetClubLeaderboardPage(this DbSet clubs, int page) - => clubs.AsNoTracking().OrderByDescending(x => x.Xp).Skip(page * 9).Take(9).ToList(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/CurrencyTransactionExtensions.cs b/src/Ellie.Bot.Db/Extensions/CurrencyTransactionExtensions.cs deleted file mode 100644 index 72426da..0000000 --- a/src/Ellie.Bot.Db/Extensions/CurrencyTransactionExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class CurrencyTransactionExtensions -{ - public static Task> GetPageFor( - this DbSet set, - ulong userId, - int page) - => set.ToLinqToDBTable() - .Where(x => x.UserId == userId) - .OrderByDescending(x => x.DateAdded) - .Skip(15 * page) - .Take(15) - .ToListAsyncLinqToDB(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/DbExtensions.cs b/src/Ellie.Bot.Db/Extensions/DbExtensions.cs deleted file mode 100644 index 5c7c843..0000000 --- a/src/Ellie.Bot.Db/Extensions/DbExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class DbExtensions -{ - public static T GetById(this DbSet set, int id) - where T : DbEntity - => set.FirstOrDefault(x => x.Id == id); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/DiscordUserExtensions.cs b/src/Ellie.Bot.Db/Extensions/DiscordUserExtensions.cs deleted file mode 100644 index 2ffb6da..0000000 --- a/src/Ellie.Bot.Db/Extensions/DiscordUserExtensions.cs +++ /dev/null @@ -1,126 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Db.Models; - -namespace Ellie.Db; - -public static class DiscordUserExtensions -{ - public static Task GetByUserIdAsync( - this IQueryable set, - ulong userId) - => set.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId); - - public static void EnsureUserCreated( - this DbContext ctx, - ulong userId, - string username, - string discrim, - string avatarId) - => ctx.GetTable() - .InsertOrUpdate( - () => new() - { - UserId = userId, - Username = username, - Discriminator = discrim, - AvatarId = avatarId, - TotalXp = 0, - CurrencyAmount = 0 - }, - old => new() - { - Username = username, - Discriminator = discrim, - AvatarId = avatarId - }, - () => new() - { - UserId = userId - }); - - public static Task EnsureUserCreatedAsync( - this DbContext ctx, - ulong userId) - => ctx.GetTable() - .InsertOrUpdateAsync( - () => new() - { - UserId = userId, - Username = "Unknown", - Discriminator = "????", - AvatarId = string.Empty, - TotalXp = 0, - CurrencyAmount = 0 - }, - old => new() - { - - }, - () => new() - { - UserId = userId - }); - - //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown - public static DiscordUser GetOrCreateUser( - this DbContext ctx, - ulong userId, - string username, - string discrim, - string avatarId, - Func, IQueryable> includes = null) - { - ctx.EnsureUserCreated(userId, username, discrim, avatarId); - - IQueryable queryable = ctx.Set(); - if (includes is not null) - queryable = includes(queryable); - return queryable.First(u => u.UserId == userId); - } - - - public static int GetUserGlobalRank(this DbSet users, ulong id) - => users.AsQueryable() - .Where(x => x.TotalXp - > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault()) - .Count() - + 1; - - public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet users, int page) - => users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * 9).Take(9).AsEnumerable().ToArray(); - - public static List GetTopRichest( - this DbSet users, - ulong botId, - int count, - int page = 0) - => users.AsQueryable() - .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) - .OrderByDescending(c => c.CurrencyAmount) - .Skip(page * 9) - .Take(count) - .ToList(); - - public static async Task GetUserCurrencyAsync(this DbSet users, ulong userId) - => (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0; - - public static void RemoveFromMany(this DbSet users, IEnumerable ids) - { - var items = users.AsQueryable().Where(x => ids.Contains(x.UserId)); - foreach (var item in items) - item.CurrencyAmount = 0; - } - - public static decimal GetTotalCurrency(this DbSet users) - => users.Sum((Func)(x => x.CurrencyAmount)); - - public static decimal GetTopOnePercentCurrency(this DbSet users, ulong botId) - => users.AsQueryable() - .Where(x => x.UserId != botId) - .OrderByDescending(x => x.CurrencyAmount) - .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) - .Sum(x => x.CurrencyAmount); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/EllieExpressionExtensions.cs b/src/Ellie.Bot.Db/Extensions/EllieExpressionExtensions.cs deleted file mode 100644 index 79b8f1e..0000000 --- a/src/Ellie.Bot.Db/Extensions/EllieExpressionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -using LinqToDB; -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class EllieExpressionExtensions -{ - public static int ClearFromGuild(this DbSet exprs, ulong guildId) - => exprs.Delete(x => x.GuildId == guildId); - - public static IEnumerable ForId(this DbSet exprs, ulong id) - => exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/GuildConfigExtensions.cs b/src/Ellie.Bot.Db/Extensions/GuildConfigExtensions.cs deleted file mode 100644 index cb2da22..0000000 --- a/src/Ellie.Bot.Db/Extensions/GuildConfigExtensions.cs +++ /dev/null @@ -1,229 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db.Models; -using Ellie.Services.Database; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class GuildConfigExtensions -{ - private static List DefaultWarnPunishments - => new() - { - new() - { - Count = 3, - Punishment = PunishmentAction.Kick - }, - new() - { - Count = 5, - Punishment = PunishmentAction.Ban - } - }; - - /// - /// Gets full stream role settings for the guild with the specified id. - /// - /// Db Context - /// Id of the guild to get stream role settings for. - /// Guild'p stream role settings - public static StreamRoleSettings GetStreamRoleSettings(this DbContext ctx, ulong guildId) - { - var conf = ctx.GuildConfigsForId(guildId, - set => set.Include(y => y.StreamRole) - .Include(y => y.StreamRole.Whitelist) - .Include(y => y.StreamRole.Blacklist)); - - if (conf.StreamRole is null) - conf.StreamRole = new(); - - return conf.StreamRole; - } - - private static IQueryable IncludeEverything(this DbSet configs) - => configs.AsQueryable() - .AsSplitQuery() - .Include(gc => gc.CommandCooldowns) - .Include(gc => gc.FollowedStreams) - .Include(gc => gc.StreamRole) - .Include(gc => gc.XpSettings) - .ThenInclude(x => x.ExclusionList) - .Include(gc => gc.DelMsgOnCmdChannels); - - public static IEnumerable GetAllGuildConfigs( - this DbSet configs, - IReadOnlyList availableGuilds) - => configs.IncludeEverything().AsNoTracking().Where(x => availableGuilds.Contains(x.GuildId)).ToList(); - - /// - /// Gets and creates if it doesn't exist a config for a guild. - /// - /// Context - /// Id of the guide - /// Use to manipulate the set however you want. Pass null to include everything - /// Config for the guild - public static GuildConfig GuildConfigsForId( - this DbContext ctx, - ulong guildId, - Func, IQueryable> includes) - { - GuildConfig config; - - if (includes is null) - config = ctx.Set().IncludeEverything().FirstOrDefault(c => c.GuildId == guildId); - else - { - var set = includes(ctx.Set()); - config = set.FirstOrDefault(c => c.GuildId == guildId); - } - - if (config is null) - { - ctx.Set().Add(config = new() - { - GuildId = guildId, - Permissions = Permissionv2.GetDefaultPermlist, - WarningsInitialized = true, - WarnPunishments = DefaultWarnPunishments - }); - ctx.SaveChanges(); - } - - if (!config.WarningsInitialized) - { - config.WarningsInitialized = true; - config.WarnPunishments = DefaultWarnPunishments; - } - - return config; - - // ctx.GuildConfigs - // .ToLinqToDBTable() - // .InsertOrUpdate(() => new() - // { - // GuildId = guildId, - // Permissions = Permissionv2.GetDefaultPermlist, - // WarningsInitialized = true, - // WarnPunishments = DefaultWarnPunishments - // }, - // _ => new(), - // () => new() - // { - // GuildId = guildId - // }); - // - // if(includes is null) - // return ctx.GuildConfigs - // .ToLinqToDBTable() - // .First(x => x.GuildId == guildId); - } - - public static LogSetting LogSettingsFor(this DbContext ctx, ulong guildId) - { - var logSetting = ctx.Set() - .AsQueryable() - .Include(x => x.LogIgnores) - .Where(x => x.GuildId == guildId) - .FirstOrDefault(); - - if (logSetting is null) - { - ctx.Set() - .Add(logSetting = new() - { - GuildId = guildId - }); - ctx.SaveChanges(); - } - - return logSetting; - } - - public static IEnumerable PermissionsForAll(this DbSet configs, List include) - { - var query = configs.AsQueryable().Where(x => include.Contains(x.GuildId)).Include(gc => gc.Permissions); - - return query.ToList(); - } - - public static GuildConfig GcWithPermissionsFor(this DbContext ctx, ulong guildId) - { - var config = ctx.Set().AsQueryable() - .Where(gc => gc.GuildId == guildId) - .Include(gc => gc.Permissions) - .FirstOrDefault(); - - if (config is null) // if there is no guildconfig, create new one - { - ctx.Set().Add(config = new() - { - GuildId = guildId, - Permissions = Permissionv2.GetDefaultPermlist - }); - ctx.SaveChanges(); - } - else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones - { - config.Permissions = Permissionv2.GetDefaultPermlist; - ctx.SaveChanges(); - } - - return config; - } - - public static IEnumerable GetFollowedStreams(this DbSet configs) - => configs.AsQueryable().Include(x => x.FollowedStreams).SelectMany(gc => gc.FollowedStreams).ToArray(); - - public static IEnumerable GetFollowedStreams(this DbSet configs, List included) - => configs.AsQueryable() - .Where(gc => included.Contains(gc.GuildId)) - .Include(gc => gc.FollowedStreams) - .SelectMany(gc => gc.FollowedStreams) - .ToList(); - - public static void SetCleverbotEnabled(this DbSet configs, ulong id, bool cleverbotEnabled) - { - var conf = configs.FirstOrDefault(gc => gc.GuildId == id); - - if (conf is null) - return; - - conf.CleverbotEnabled = cleverbotEnabled; - } - - public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId) - { - var gc = ctx.GuildConfigsForId(guildId, - set => set.Include(x => x.XpSettings) - .ThenInclude(x => x.RoleRewards) - .Include(x => x.XpSettings) - .ThenInclude(x => x.CurrencyRewards) - .Include(x => x.XpSettings) - .ThenInclude(x => x.ExclusionList)); - - if (gc.XpSettings is null) - gc.XpSettings = new(); - - return gc.XpSettings; - } - - public static IEnumerable GetGeneratingChannels(this DbSet configs) - => configs.AsQueryable() - .Include(x => x.GenerateCurrencyChannelIds) - .Where(x => x.GenerateCurrencyChannelIds.Any()) - .SelectMany(x => x.GenerateCurrencyChannelIds) - .Select(x => new GeneratingChannel - { - ChannelId = x.ChannelId, - GuildId = x.GuildConfig.GuildId - }) - .ToArray(); - - public class GeneratingChannel - { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/QuoteExtensions.cs b/src/Ellie.Bot.Db/Extensions/QuoteExtensions.cs deleted file mode 100644 index 5b6a4bd..0000000 --- a/src/Ellie.Bot.Db/Extensions/QuoteExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class QuoteExtensions -{ - public static IEnumerable GetForGuild(this DbSet quotes, ulong guildId) - => quotes.AsQueryable().Where(x => x.GuildId == guildId); - - public static IReadOnlyCollection GetGroup( - this DbSet quotes, - ulong guildId, - int page, - OrderType order) - { - var q = quotes.AsQueryable().Where(x => x.GuildId == guildId); - if (order == OrderType.Keyword) - q = q.OrderBy(x => x.Keyword); - else - q = q.OrderBy(x => x.Id); - - return q.Skip(15 * page).Take(15).ToArray(); - } - - public static async Task GetRandomQuoteByKeywordAsync( - this DbSet quotes, - ulong guildId, - string keyword) - { - // todo figure shuffle out - return (await quotes.AsQueryable().Where(q => q.GuildId == guildId && q.Keyword == keyword).ToListAsync()) - .Shuffle() - .FirstOrDefault(); - } - - public static async Task SearchQuoteKeywordTextAsync( - this DbSet quotes, - ulong guildId, - string keyword, - string text) - { - var rngk = new EllieRandom(); - return (await quotes.AsQueryable() - .Where(q => q.GuildId == guildId - && (keyword == null || q.Keyword == keyword) - && (EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%") - || EF.Functions.Like(q.AuthorName, text))) - .ToListAsync()) - .OrderBy(_ => rngk.Next()) - .FirstOrDefault(); - } - - public static void RemoveAllByKeyword(this DbSet quotes, ulong guildId, string keyword) - => quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword)); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/ReminderExtensions.cs b/src/Ellie.Bot.Db/Extensions/ReminderExtensions.cs deleted file mode 100644 index 8ce2273..0000000 --- a/src/Ellie.Bot.Db/Extensions/ReminderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class ReminderExtensions -{ - public static IEnumerable GetIncludedReminders( - this DbSet reminders, - IEnumerable guildIds) - => reminders.AsQueryable().Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0).ToList(); - - public static IEnumerable RemindersFor(this DbSet reminders, ulong userId, int page) - => reminders.AsQueryable().Where(x => x.UserId == userId).OrderBy(x => x.DateAdded).Skip(page * 10).Take(10); - - public static IEnumerable RemindersForServer(this DbSet reminders, ulong serverId, int page) - => reminders.AsQueryable() - .Where(x => x.ServerId == serverId) - .OrderBy(x => x.DateAdded) - .Skip(page * 10) - .Take(10); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/SelfAssignableRolesExtensions.cs b/src/Ellie.Bot.Db/Extensions/SelfAssignableRolesExtensions.cs deleted file mode 100644 index 2d3e54e..0000000 --- a/src/Ellie.Bot.Db/Extensions/SelfAssignableRolesExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class SelfAssignableRolesExtensions -{ - public static bool DeleteByGuildAndRoleId(this DbSet roles, ulong guildId, ulong roleId) - { - var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId); - - if (role is null) - return false; - - roles.Remove(role); - return true; - } - - public static IReadOnlyCollection GetFromGuild(this DbSet roles, ulong guildId) - => roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/UserXpExtensions.cs b/src/Ellie.Bot.Db/Extensions/UserXpExtensions.cs deleted file mode 100644 index 4951b5c..0000000 --- a/src/Ellie.Bot.Db/Extensions/UserXpExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class UserXpExtensions -{ - public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId) - { - var usr = ctx.Set().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); - - if (usr is null) - { - ctx.Add(usr = new() - { - Xp = 0, - UserId = userId, - NotifyOnLevelUp = XpNotificationLocation.None, - GuildId = guildId - }); - } - - return usr; - } - - public static List GetUsersFor(this DbSet xps, ulong guildId, int page) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Skip(page * 9) - .Take(9) - .ToList(); - - public static List GetTopUserXps(this DbSet xps, ulong guildId, int count) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Take(count) - .ToList(); - - public static int GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId - && x.Xp + x.AwardedXp - > xps.AsQueryable() - .Where(y => y.UserId == userId && y.GuildId == guildId) - .Select(y => y.Xp + y.AwardedXp) - .FirstOrDefault()) - .Count() - + 1; - - public static void ResetGuildUserXp(this DbSet xps, ulong userId, ulong guildId) - => xps.Delete(x => x.UserId == userId && x.GuildId == guildId); - - public static void ResetGuildXp(this DbSet xps, ulong guildId) - => xps.Delete(x => x.GuildId == guildId); - - public static async Task GetLevelDataFor(this ITable userXp, ulong guildId, ulong userId) - => await userXp - .Where(x => x.GuildId == guildId && x.UserId == userId) - .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs - ? new(uxs.Xp + uxs.AwardedXp) - : new(0); - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Extensions/WarningExtensions.cs b/src/Ellie.Bot.Db/Extensions/WarningExtensions.cs deleted file mode 100644 index ccb1b72..0000000 --- a/src/Ellie.Bot.Db/Extensions/WarningExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class WarningExtensions -{ - public static Warning[] ForId(this DbSet warnings, ulong guildId, ulong userId) - { - var query = warnings.AsQueryable() - .Where(x => x.GuildId == guildId && x.UserId == userId) - .OrderByDescending(x => x.DateAdded); - - return query.ToArray(); - } - - public static bool Forgive( - this DbSet warnings, - ulong guildId, - ulong userId, - string mod, - int index) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - var warn = warnings.AsQueryable() - .Where(x => x.GuildId == guildId && x.UserId == userId) - .OrderByDescending(x => x.DateAdded) - .Skip(index) - .FirstOrDefault(); - - if (warn is null || warn.Forgiven) - return false; - - warn.Forgiven = true; - warn.ForgivenBy = mod; - return true; - } - - public static async Task ForgiveAll( - this DbSet warnings, - ulong guildId, - ulong userId, - string mod) - => await warnings.AsQueryable() - .Where(x => x.GuildId == guildId && x.UserId == userId) - .ForEachAsync(x => - { - if (x.Forgiven != true) - { - x.Forgiven = true; - x.ForgivenBy = mod; - } - }); - - public static Warning[] GetForGuild(this DbSet warnings, ulong id) - => warnings.AsQueryable().Where(x => x.GuildId == id).ToArray(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/GlobalUsings.cs b/src/Ellie.Bot.Db/GlobalUsings.cs index b445dd6..6e365e5 100644 --- a/src/Ellie.Bot.Db/GlobalUsings.cs +++ b/src/Ellie.Bot.Db/GlobalUsings.cs @@ -1,4 +1,3 @@ // // ellie global using Ellie; -global using Ellie.Services; global using Ellise.Common; \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Helpers/ActivityType.cs b/src/Ellie.Bot.Db/Helpers/ActivityType.cs deleted file mode 100644 index 6850963..0000000 --- a/src/Ellie.Bot.Db/Helpers/ActivityType.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Bot.Db; - -public enum ActivityType -{ - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Helpers/GuildPerm.cs b/src/Ellie.Bot.Db/Helpers/GuildPerm.cs deleted file mode 100644 index 5ab60e9..0000000 --- a/src/Ellie.Bot.Db/Helpers/GuildPerm.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Ellie.Bot.Db; - -[Flags] -public enum GuildPerm : ulong -{ - CreateInstantInvite = 1, - KickMembers = 2, - BanMembers = 4, - Administrator = 8, - ManageChannels = 16, // 0x0000000000000010 - ManageGuild = 32, // 0x0000000000000020 - ViewGuildInsights = 524288, // 0x0000000000080000 - AddReactions = 64, // 0x0000000000000040 - ViewAuditLog = 128, // 0x0000000000000080 - ViewChannel = 1024, // 0x0000000000000400 - SendMessages = 2048, // 0x0000000000000800 - SendTTSMessages = 4096, // 0x0000000000001000 - ManageMessages = 8192, // 0x0000000000002000 - EmbedLinks = 16384, // 0x0000000000004000 - AttachFiles = 32768, // 0x0000000000008000 - ReadMessageHistory = 65536, // 0x0000000000010000 - MentionEveryone = 131072, // 0x0000000000020000 - UseExternalEmojis = 262144, // 0x0000000000040000 - Connect = 1048576, // 0x0000000000100000 - Speak = 2097152, // 0x0000000000200000 - MuteMembers = 4194304, // 0x0000000000400000 - DeafenMembers = 8388608, // 0x0000000000800000 - MoveMembers = 16777216, // 0x0000000001000000 - UseVAD = 33554432, // 0x0000000002000000 - PrioritySpeaker = 256, // 0x0000000000000100 - Stream = 512, // 0x0000000000000200 - ChangeNickname = 67108864, // 0x0000000004000000 - ManageNicknames = 134217728, // 0x0000000008000000 - ManageRoles = 268435456, // 0x0000000010000000 - ManageWebhooks = 536870912, // 0x0000000020000000 - ManageEmojisAndStickers = 1073741824, // 0x0000000040000000 - UseApplicationCommands = 2147483648, // 0x0000000080000000 - RequestToSpeak = 4294967296, // 0x0000000100000000 - ManageEvents = 8589934592, // 0x0000000200000000 - ManageThreads = 17179869184, // 0x0000000400000000 - CreatePublicThreads = 34359738368, // 0x0000000800000000 - CreatePrivateThreads = 68719476736, // 0x0000001000000000 - UseExternalStickers = 137438953472, // 0x0000002000000000 - SendMessagesInThreads = 274877906944, // 0x0000004000000000 - StartEmbeddedActivities = 549755813888, // 0x0000008000000000 - ModerateMembers = 1099511627776, // 0x0000010000000000 -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/LevelStats.cs b/src/Ellie.Bot.Db/LevelStats.cs deleted file mode 100644 index 738794f..0000000 --- a/src/Ellie.Bot.Db/LevelStats.cs +++ /dev/null @@ -1,41 +0,0 @@ -#nullable disable - -namespace Ellie.Db; - -public readonly struct LevelStats -{ - public const int XP_REQUIRED_LVL_1 = 36; - - public long Level { get; } - public long LevelXp { get; } - public long RequiredXp { get; } - public long TotalXp { get; } - - public LevelStats(long xp) - { - if (xp < 0) - xp = 0; - - TotalXp = xp; - - const int baseXp = XP_REQUIRED_LVL_1; - - var required = baseXp; - var totalXp = 0; - var lvl = 1; - while (true) - { - required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1))); - - if (required + totalXp > xp) - break; - - totalXp += required; - lvl++; - } - - Level = lvl - 1; - LevelXp = xp - totalXp; - RequiredXp = required; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/AutoCommand.cs b/src/Ellie.Bot.Db/Models/AutoCommand.cs deleted file mode 100644 index 7968d69..0000000 --- a/src/Ellie.Bot.Db/Models/AutoCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class AutoCommand : DbEntity -{ - public string CommandText { get; set; } - public ulong ChannelId { get; set; } - public string ChannelName { get; set; } - public ulong? GuildId { get; set; } - public string GuildName { get; set; } - public ulong? VoiceChannelId { get; set; } - public string VoiceChannelName { get; set; } - public int Interval { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/AutoPublishChannel.cs b/src/Ellie.Bot.Db/Models/AutoPublishChannel.cs deleted file mode 100644 index 6b364fa..0000000 --- a/src/Ellie.Bot.Db/Models/AutoPublishChannel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Db.Models; - -public class AutoPublishChannel -{ - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/AutoTranslateChannel.cs b/src/Ellie.Bot.Db/Models/AutoTranslateChannel.cs deleted file mode 100644 index 1b1f177..0000000 --- a/src/Ellie.Bot.Db/Models/AutoTranslateChannel.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class AutoTranslateChannel : DbEntity -{ - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public bool AutoDelete { get; set; } - public IList Users { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/AutoTranslateUser.cs b/src/Ellie.Bot.Db/Models/AutoTranslateUser.cs deleted file mode 100644 index b106f7e..0000000 --- a/src/Ellie.Bot.Db/Models/AutoTranslateUser.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class AutoTranslateUser : DbEntity -{ - public int ChannelId { get; set; } - public AutoTranslateChannel Channel { get; set; } - public ulong UserId { get; set; } - public string Source { get; set; } - public string Target { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/BlacklistEntry.cs b/src/Ellie.Bot.Db/Models/BlacklistEntry.cs deleted file mode 100644 index 74dcd0b..0000000 --- a/src/Ellie.Bot.Db/Models/BlacklistEntry.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class BlacklistEntry : DbEntity -{ - public ulong ItemId { get; set; } - public BlacklistType Type { get; set; } -} - -public enum BlacklistType -{ - Server, - Channel, - User -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/CommandAlias.cs b/src/Ellie.Bot.Db/Models/CommandAlias.cs deleted file mode 100644 index f83fed5..0000000 --- a/src/Ellie.Bot.Db/Models/CommandAlias.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class CommandAlias : DbEntity -{ - public string Trigger { get; set; } - public string Mapping { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/CommandCooldown.cs b/src/Ellie.Bot.Db/Models/CommandCooldown.cs deleted file mode 100644 index 438ed4a..0000000 --- a/src/Ellie.Bot.Db/Models/CommandCooldown.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class CommandCooldown : DbEntity -{ - public int Seconds { get; set; } - public string CommandName { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/CurrencyTransaction.cs b/src/Ellie.Bot.Db/Models/CurrencyTransaction.cs deleted file mode 100644 index 9fced0c..0000000 --- a/src/Ellie.Bot.Db/Models/CurrencyTransaction.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class CurrencyTransaction : DbEntity -{ - public long Amount { get; set; } - public string Note { get; set; } - public ulong UserId { get; set; } - public string Type { get; set; } - public string Extra { get; set; } - public ulong? OtherId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/DbEntity.cs b/src/Ellie.Bot.Db/Models/DbEntity.cs deleted file mode 100644 index 369bcbf..0000000 --- a/src/Ellie.Bot.Db/Models/DbEntity.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using System.ComponentModel.DataAnnotations; - -namespace Ellie.Services.Database.Models; - -public class DbEntity -{ - [Key] - public int Id { get; set; } - - public DateTime? DateAdded { get; set; } = DateTime.UtcNow; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/DelMsgOnCmdChannel.cs b/src/Ellie.Bot.Db/Models/DelMsgOnCmdChannel.cs deleted file mode 100644 index 564da2f..0000000 --- a/src/Ellie.Bot.Db/Models/DelMsgOnCmdChannel.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class DelMsgOnCmdChannel : DbEntity -{ - public ulong ChannelId { get; set; } - public bool State { get; set; } - - public override int GetHashCode() - => ChannelId.GetHashCode(); - - public override bool Equals(object obj) - => obj is DelMsgOnCmdChannel x && x.ChannelId == ChannelId; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/DiscordPermOverride.cs b/src/Ellie.Bot.Db/Models/DiscordPermOverride.cs deleted file mode 100644 index 3edb006..0000000 --- a/src/Ellie.Bot.Db/Models/DiscordPermOverride.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using Ellie.Bot.Db; - -namespace Ellie.Services.Database.Models; - -public class DiscordPermOverride : DbEntity -{ - public GuildPerm Perm { get; set; } - - public ulong? GuildId { get; set; } - public string Command { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/DiscordUser.cs b/src/Ellie.Bot.Db/Models/DiscordUser.cs deleted file mode 100644 index d62dbc9..0000000 --- a/src/Ellie.Bot.Db/Models/DiscordUser.cs +++ /dev/null @@ -1,31 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Db.Models; - -// FUTURE remove LastLevelUp from here and UserXpStats -public class DiscordUser : DbEntity -{ - public ulong UserId { get; set; } - public string Username { get; set; } - public string Discriminator { get; set; } - public string AvatarId { get; set; } - - public int? ClubId { get; set; } - public ClubInfo Club { get; set; } - public bool IsClubAdmin { get; set; } - - public long TotalXp { get; set; } - public XpNotificationLocation NotifyOnLevelUp { get; set; } - - public long CurrencyAmount { get; set; } - - public override bool Equals(object obj) - => obj is DiscordUser du ? du.UserId == UserId : false; - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override string ToString() - => Username + "#" + Discriminator; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/Event.cs b/src/Ellie.Bot.Db/Models/Event.cs deleted file mode 100644 index c81cc2b..0000000 --- a/src/Ellie.Bot.Db/Models/Event.cs +++ /dev/null @@ -1,49 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class CurrencyEvent -{ - public enum Type - { - Reaction, - - GameStatus - //NotRaid, - } - - public ulong ServerId { get; set; } - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } - public Type EventType { get; set; } - - /// - /// Amount of currency that the user will be rewarded. - /// - public long Amount { get; set; } - - /// - /// Maximum amount of currency that can be handed out. - /// - public long PotSize { get; set; } - - public List AwardedUsers { get; set; } - - /// - /// Used as extra data storage for events which need it. - /// - public ulong ExtraId { get; set; } - - /// - /// May be used for some future event. - /// - public ulong ExtraId2 { get; set; } - - /// - /// May be used for some future event. - /// - public string ExtraString { get; set; } -} - -public class AwardedUser -{ -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/FeedSub.cs b/src/Ellie.Bot.Db/Models/FeedSub.cs deleted file mode 100644 index 65578fe..0000000 --- a/src/Ellie.Bot.Db/Models/FeedSub.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class FeedSub : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public ulong ChannelId { get; set; } - public string Url { get; set; } - - public string Message { get; set; } - - public override int GetHashCode() - => Url.GetHashCode(StringComparison.InvariantCulture) ^ GuildConfigId.GetHashCode(); - - public override bool Equals(object obj) - => obj is FeedSub s && s.Url.ToLower() == Url.ToLower() && s.GuildConfigId == GuildConfigId; -} diff --git a/src/Ellie.Bot.Db/Models/FollowedStream.cs b/src/Ellie.Bot.Db/Models/FollowedStream.cs deleted file mode 100644 index f4ef3d6..0000000 --- a/src/Ellie.Bot.Db/Models/FollowedStream.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Db.Models; - -public class FollowedStream : DbEntity -{ - public enum FType - { - Twitch = 0, - Picarto = 3, - Youtube = 4, - Facebook = 5, - Trovo = 6 - } - - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public string Username { get; set; } - public FType Type { get; set; } - public string Message { get; set; } - - protected bool Equals(FollowedStream other) - => ChannelId == other.ChannelId - && Username.Trim().ToUpperInvariant() == other.Username.Trim().ToUpperInvariant() - && Type == other.Type; - - public override int GetHashCode() - => HashCode.Combine(ChannelId, Username, (int)Type); - - public override bool Equals(object obj) - => obj is FollowedStream fs && Equals(fs); - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/GCChannelId.cs b/src/Ellie.Bot.Db/Models/GCChannelId.cs deleted file mode 100644 index 3322dd5..0000000 --- a/src/Ellie.Bot.Db/Models/GCChannelId.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class GCChannelId : DbEntity -{ - public GuildConfig GuildConfig { get; set; } - public ulong ChannelId { get; set; } - - public override bool Equals(object obj) - => obj is GCChannelId gc && gc.ChannelId == ChannelId; - - public override int GetHashCode() - => ChannelId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/GamblingStats.cs b/src/Ellie.Bot.Db/Models/GamblingStats.cs deleted file mode 100644 index ca9a7a5..0000000 --- a/src/Ellie.Bot.Db/Models/GamblingStats.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class GamblingStats : DbEntity -{ - public string Feature { get; set; } - public decimal Bet { get; set; } - public decimal PaidOut { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/GroupName.cs b/src/Ellie.Bot.Db/Models/GroupName.cs deleted file mode 100644 index 9c4c831..0000000 --- a/src/Ellie.Bot.Db/Models/GroupName.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class GroupName : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public int Number { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/GuildConfig.cs b/src/Ellie.Bot.Db/Models/GuildConfig.cs deleted file mode 100644 index afa1edd..0000000 --- a/src/Ellie.Bot.Db/Models/GuildConfig.cs +++ /dev/null @@ -1,108 +0,0 @@ -#nullable disable -using Ellie.Db.Models; - -namespace Ellie.Services.Database.Models; - -public class GuildConfig : DbEntity -{ - public ulong GuildId { get; set; } - - public string Prefix { get; set; } - - public bool DeleteMessageOnCommand { get; set; } - public HashSet DelMsgOnCmdChannels { get; set; } = new(); - - public string AutoAssignRoleIds { get; set; } - - //greet stuff - public int AutoDeleteGreetMessagesTimer { get; set; } = 30; - public int AutoDeleteByeMessagesTimer { get; set; } = 30; - - public ulong GreetMessageChannelId { get; set; } - public ulong ByeMessageChannelId { get; set; } - - public bool SendDmGreetMessage { get; set; } - public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; - - public bool SendChannelGreetMessage { get; set; } - public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; - - public bool SendChannelByeMessage { get; set; } - public string ChannelByeMessageText { get; set; } = "%user% has left!"; - - //self assignable roles - public bool ExclusiveSelfAssignedRoles { get; set; } - public bool AutoDeleteSelfAssignedRoleMessages { get; set; } - - //stream notifications - public HashSet FollowedStreams { get; set; } = new(); - - //currencyGeneration - public HashSet GenerateCurrencyChannelIds { get; set; } = new(); - - public List Permissions { get; set; } - public bool VerbosePermissions { get; set; } = true; - public string PermissionRole { get; set; } - - public HashSet CommandCooldowns { get; set; } = new(); - - //filtering - public bool FilterInvites { get; set; } - public bool FilterLinks { get; set; } - public HashSet FilterInvitesChannelIds { get; set; } = new(); - public HashSet FilterLinksChannelIds { get; set; } = new(); - - //public bool FilterLinks { get; set; } - //public HashSet FilterLinksChannels { get; set; } = new HashSet(); - - public bool FilterWords { get; set; } - public HashSet FilteredWords { get; set; } = new(); - public HashSet FilterWordsChannelIds { get; set; } = new(); - - public HashSet MutedUsers { get; set; } = new(); - - public string MuteRoleName { get; set; } - public bool CleverbotEnabled { get; set; } - - public AntiRaidSetting AntiRaidSetting { get; set; } - public AntiSpamSetting AntiSpamSetting { get; set; } - public AntiAltSetting AntiAltSetting { get; set; } - - public string Locale { get; set; } - public string TimeZoneId { get; set; } - - public HashSet UnmuteTimers { get; set; } = new(); - public HashSet UnbanTimer { get; set; } = new(); - public HashSet UnroleTimer { get; set; } = new(); - public HashSet VcRoleInfos { get; set; } - public HashSet CommandAliases { get; set; } = new(); - public List WarnPunishments { get; set; } = new(); - public bool WarningsInitialized { get; set; } - public HashSet SlowmodeIgnoredUsers { get; set; } - public HashSet SlowmodeIgnoredRoles { get; set; } - - public List ShopEntries { get; set; } - public ulong? GameVoiceChannel { get; set; } - public bool VerboseErrors { get; set; } = true; - - public StreamRoleSettings StreamRole { get; set; } - - public XpSettings XpSettings { get; set; } - public List FeedSubs { get; set; } = new(); - public bool NotifyStreamOffline { get; set; } - public bool DeleteStreamOnlineMessage { get; set; } - public List SelfAssignableRoleGroupNames { get; set; } - public int WarnExpireHours { get; set; } - public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear; - - public bool DisableGlobalExpressions { get; set; } = false; - - #region Boost Message - - public bool SendBoostMessage { get; set; } - public string BoostMessage { get; set; } = "%user% just boosted this server!"; - public ulong BoostMessageChannelId { get; set; } - public int BoostMessageDeleteAfter { get; set; } - - #endregion -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/IgnoredLogItem.cs b/src/Ellie.Bot.Db/Models/IgnoredLogItem.cs deleted file mode 100644 index 254ac46..0000000 --- a/src/Ellie.Bot.Db/Models/IgnoredLogItem.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class IgnoredLogItem : DbEntity -{ - public int LogSettingId { get; set; } - public LogSetting LogSetting { get; set; } - public ulong LogItemId { get; set; } - public IgnoredItemType ItemType { get; set; } -} - -public enum IgnoredItemType -{ - Channel, - User -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/IgnoredVoicePresenceChannel.cs b/src/Ellie.Bot.Db/Models/IgnoredVoicePresenceChannel.cs deleted file mode 100644 index f21fe6a..0000000 --- a/src/Ellie.Bot.Db/Models/IgnoredVoicePresenceChannel.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class IgnoredVoicePresenceChannel : DbEntity -{ - public LogSetting LogSetting { get; set; } - public ulong ChannelId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/ImageOnlyChannel.cs b/src/Ellie.Bot.Db/Models/ImageOnlyChannel.cs deleted file mode 100644 index 3e68b94..0000000 --- a/src/Ellie.Bot.Db/Models/ImageOnlyChannel.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class ImageOnlyChannel : DbEntity -{ - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public OnlyChannelType Type { get; set; } -} - -public enum OnlyChannelType -{ - Image, - Link -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/LogSetting.cs b/src/Ellie.Bot.Db/Models/LogSetting.cs deleted file mode 100644 index ec417a8..0000000 --- a/src/Ellie.Bot.Db/Models/LogSetting.cs +++ /dev/null @@ -1,37 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class LogSetting : DbEntity -{ - public List LogIgnores { get; set; } = new(); - - public ulong GuildId { get; set; } - public ulong? LogOtherId { get; set; } - public ulong? MessageUpdatedId { get; set; } - public ulong? MessageDeletedId { get; set; } - - public ulong? UserJoinedId { get; set; } - public ulong? UserLeftId { get; set; } - public ulong? UserBannedId { get; set; } - public ulong? UserUnbannedId { get; set; } - public ulong? UserUpdatedId { get; set; } - - public ulong? ChannelCreatedId { get; set; } - public ulong? ChannelDestroyedId { get; set; } - public ulong? ChannelUpdatedId { get; set; } - - - public ulong? ThreadDeletedId { get; set; } - public ulong? ThreadCreatedId { get; set; } - - public ulong? UserMutedId { get; set; } - - //userpresence - public ulong? LogUserPresenceId { get; set; } - - //voicepresence - - public ulong? LogVoicePresenceId { get; set; } - public ulong? LogVoicePresenceTTSId { get; set; } - public ulong? LogWarnsId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/NsfwBlacklistedTag.cs b/src/Ellie.Bot.Db/Models/NsfwBlacklistedTag.cs deleted file mode 100644 index 6e74b43..0000000 --- a/src/Ellie.Bot.Db/Models/NsfwBlacklistedTag.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class NsfwBlacklistedTag : DbEntity -{ - public ulong GuildId { get; set; } - public string Tag { get; set; } - - public override int GetHashCode() - => Tag.GetHashCode(StringComparison.InvariantCulture); - - public override bool Equals(object obj) - => obj is NsfwBlacklistedTag x && x.Tag == Tag; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/Permission.cs b/src/Ellie.Bot.Db/Models/Permission.cs deleted file mode 100644 index 92530b4..0000000 --- a/src/Ellie.Bot.Db/Models/Permission.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable -using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics; - -namespace Ellie.Services.Database.Models; - -[DebuggerDisplay("{PrimaryTarget}{SecondaryTarget} {SecondaryTargetName} {State} {PrimaryTargetId}")] -public class Permissionv2 : DbEntity, IIndexed -{ - public int? GuildConfigId { get; set; } - public int Index { get; set; } - - public PrimaryPermissionType PrimaryTarget { get; set; } - public ulong PrimaryTargetId { get; set; } - - public SecondaryPermissionType SecondaryTarget { get; set; } - public string SecondaryTargetName { get; set; } - - public bool IsCustomCommand { get; set; } - - public bool State { get; set; } - - [NotMapped] - public static Permissionv2 AllowAllPerm - => new() - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = true, - Index = 0 - }; - - public static List GetDefaultPermlist - => new() - { - AllowAllPerm - }; -} - -public enum PrimaryPermissionType -{ - User, - Channel, - Role, - Server -} - -public enum SecondaryPermissionType -{ - Module, - Command, - AllModules -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/PlantedCurrency.cs b/src/Ellie.Bot.Db/Models/PlantedCurrency.cs deleted file mode 100644 index fcc6c19..0000000 --- a/src/Ellie.Bot.Db/Models/PlantedCurrency.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class PlantedCurrency : DbEntity -{ - public long Amount { get; set; } - public string Password { get; set; } - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public ulong UserId { get; set; } - public ulong MessageId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/PlaylistSong.cs b/src/Ellie.Bot.Db/Models/PlaylistSong.cs deleted file mode 100644 index 5c2dd78..0000000 --- a/src/Ellie.Bot.Db/Models/PlaylistSong.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class PlaylistSong : DbEntity -{ - public string Provider { get; set; } - public MusicType ProviderType { get; set; } - public string Title { get; set; } - public string Uri { get; set; } - public string Query { get; set; } -} - -public enum MusicType -{ - Radio, - YouTube, - Local, - Soundcloud -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/Poll.cs b/src/Ellie.Bot.Db/Models/Poll.cs deleted file mode 100644 index 9c63e53..0000000 --- a/src/Ellie.Bot.Db/Models/Poll.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class Poll : DbEntity -{ - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public string Question { get; set; } - public IndexedCollection Answers { get; set; } - public HashSet Votes { get; set; } = new(); -} - -public class PollAnswer : DbEntity, IIndexed -{ - public int Index { get; set; } - public string Text { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/PollVote.cs b/src/Ellie.Bot.Db/Models/PollVote.cs deleted file mode 100644 index c701945..0000000 --- a/src/Ellie.Bot.Db/Models/PollVote.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class PollVote : DbEntity -{ - public ulong UserId { get; set; } - public int VoteIndex { get; set; } - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override bool Equals(object obj) - => obj is PollVote p ? p.UserId == UserId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/ReactionRole.cs b/src/Ellie.Bot.Db/Models/ReactionRole.cs deleted file mode 100644 index a73bbdd..0000000 --- a/src/Ellie.Bot.Db/Models/ReactionRole.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable disable -using System.ComponentModel.DataAnnotations; - -namespace Ellie.Services.Database.Models; - -public class ReactionRoleV2 : DbEntity -{ - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - - public ulong MessageId { get; set; } - - [MaxLength(100)] - public string Emote { get; set; } - public ulong RoleId { get; set; } - public int Group { get; set; } - public int LevelReq { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/Reminder.cs b/src/Ellie.Bot.Db/Models/Reminder.cs deleted file mode 100644 index 897fbb8..0000000 --- a/src/Ellie.Bot.Db/Models/Reminder.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class Reminder : DbEntity -{ - public DateTime When { get; set; } - public ulong ChannelId { get; set; } - public ulong ServerId { get; set; } - public ulong UserId { get; set; } - public string Message { get; set; } - public bool IsPrivate { get; set; } - public ReminderType Type { get; set; } -} - -public enum ReminderType -{ - User, - Timely -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/Repeater.cs b/src/Ellie.Bot.Db/Models/Repeater.cs deleted file mode 100644 index afad1c7..0000000 --- a/src/Ellie.Bot.Db/Models/Repeater.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class Repeater -{ - public int Id { get; set; } - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public ulong? LastMessageId { get; set; } - public string Message { get; set; } - public TimeSpan Interval { get; set; } - public TimeSpan? StartTimeOfDay { get; set; } - public bool NoRedundant { get; set; } - public DateTime DateAdded { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/RotatingPlayingStatus.cs b/src/Ellie.Bot.Db/Models/RotatingPlayingStatus.cs deleted file mode 100644 index 4e831bf..0000000 --- a/src/Ellie.Bot.Db/Models/RotatingPlayingStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -using Ellie.Bot.Db; - -namespace Ellie.Services.Database.Models; - -public class RotatingPlayingStatus : DbEntity -{ - public string Status { get; set; } - public ActivityType Type { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/SelfAssignableRole.cs b/src/Ellie.Bot.Db/Models/SelfAssignableRole.cs deleted file mode 100644 index 9f2af77..0000000 --- a/src/Ellie.Bot.Db/Models/SelfAssignableRole.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class SelfAssignedRole : DbEntity -{ - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } - - public int Group { get; set; } - public int LevelRequirement { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/ShopEntry.cs b/src/Ellie.Bot.Db/Models/ShopEntry.cs deleted file mode 100644 index 8a8063b..0000000 --- a/src/Ellie.Bot.Db/Models/ShopEntry.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public enum ShopEntryType -{ - Role, - - List - //Infinite_List, -} - -public class ShopEntry : DbEntity, IIndexed -{ - public int Index { get; set; } - public int Price { get; set; } - public string Name { get; set; } - public ulong AuthorId { get; set; } - - public ShopEntryType Type { get; set; } - - //role - public string RoleName { get; set; } - public ulong RoleId { get; set; } - - //list - public HashSet Items { get; set; } = new(); - public ulong? RoleRequirement { get; set; } -} - -public class ShopEntryItem : DbEntity -{ - public string Text { get; set; } - - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - return false; - return ((ShopEntryItem)obj).Text == Text; - } - - public override int GetHashCode() - => Text.GetHashCode(StringComparison.InvariantCulture); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/StreamOnlineMessage.cs b/src/Ellie.Bot.Db/Models/StreamOnlineMessage.cs deleted file mode 100644 index ef6389e..0000000 --- a/src/Ellie.Bot.Db/Models/StreamOnlineMessage.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Db.Models; - -public class StreamOnlineMessage : DbEntity -{ - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } - - public FollowedStream.FType Type { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/StreamRoleSettings.cs b/src/Ellie.Bot.Db/Models/StreamRoleSettings.cs deleted file mode 100644 index d406f70..0000000 --- a/src/Ellie.Bot.Db/Models/StreamRoleSettings.cs +++ /dev/null @@ -1,68 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class StreamRoleSettings : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - /// - /// Whether the feature is enabled in the guild. - /// - public bool Enabled { get; set; } - - /// - /// Id of the role to give to the users in the role 'FromRole' when they start streaming - /// - public ulong AddRoleId { get; set; } - - /// - /// Id of the role whose users are eligible to get the 'AddRole' - /// - public ulong FromRoleId { get; set; } - - /// - /// If set, feature will only apply to users who have this keyword in their streaming status. - /// - public string Keyword { get; set; } - - /// - /// A collection of whitelisted users' IDs. Whitelisted users don't require 'keyword' in - /// order to get the stream role. - /// - public HashSet Whitelist { get; set; } = new(); - - /// - /// A collection of blacklisted users' IDs. Blacklisted useres will never get the stream role. - /// - public HashSet Blacklist { get; set; } = new(); -} - -public class StreamRoleBlacklistedUser : DbEntity -{ - public ulong UserId { get; set; } - public string Username { get; set; } - - public override bool Equals(object obj) - { - if (obj is not StreamRoleBlacklistedUser x) - return false; - - return x.UserId == UserId; - } - - public override int GetHashCode() - => UserId.GetHashCode(); -} - -public class StreamRoleWhitelistedUser : DbEntity -{ - public ulong UserId { get; set; } - public string Username { get; set; } - - public override bool Equals(object obj) - => obj is StreamRoleWhitelistedUser x ? x.UserId == UserId : false; - - public override int GetHashCode() - => UserId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/VcRoleInfo.cs b/src/Ellie.Bot.Db/Models/VcRoleInfo.cs deleted file mode 100644 index 6463622..0000000 --- a/src/Ellie.Bot.Db/Models/VcRoleInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class VcRoleInfo : DbEntity -{ - public ulong VoiceChannelId { get; set; } - public ulong RoleId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/anti/AntiAltSetting.cs b/src/Ellie.Bot.Db/Models/anti/AntiAltSetting.cs deleted file mode 100644 index 6fde1b4..0000000 --- a/src/Ellie.Bot.Db/Models/anti/AntiAltSetting.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ellie.Services.Database.Models; - -public class AntiAltSetting -{ - public int Id { get; set; } - public int GuildConfigId { get; set; } - public TimeSpan MinAge { get; set; } - public PunishmentAction Action { get; set; } - public int ActionDurationMinutes { get; set; } - public ulong? RoleId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/anti/AntiRaidSetting.cs b/src/Ellie.Bot.Db/Models/anti/AntiRaidSetting.cs deleted file mode 100644 index 1e90f63..0000000 --- a/src/Ellie.Bot.Db/Models/anti/AntiRaidSetting.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -// todo db required, nullable? -public class AntiRaidSetting : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public int UserThreshold { get; set; } - public int Seconds { get; set; } - public PunishmentAction Action { get; set; } - - /// - /// Duration of the punishment, in minutes. This works only for supported Actions, like: - /// Mute, Chatmute, Voicemute, etc... - /// - public int PunishDuration { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/anti/AntiSpamIgnore.cs b/src/Ellie.Bot.Db/Models/anti/AntiSpamIgnore.cs deleted file mode 100644 index c9fff4d..0000000 --- a/src/Ellie.Bot.Db/Models/anti/AntiSpamIgnore.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ellie.Services.Database.Models; - -public class AntiSpamIgnore : DbEntity -{ - public ulong ChannelId { get; set; } - - public override int GetHashCode() - => ChannelId.GetHashCode(); - - public override bool Equals(object obj) - => obj is AntiSpamIgnore inst ? inst.ChannelId == ChannelId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/anti/AntiSpamSetting.cs b/src/Ellie.Bot.Db/Models/anti/AntiSpamSetting.cs deleted file mode 100644 index 70d41bd..0000000 --- a/src/Ellie.Bot.Db/Models/anti/AntiSpamSetting.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ellie.Services.Database.Models; - -public class AntiSpamSetting : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public PunishmentAction Action { get; set; } - public int MessageThreshold { get; set; } = 3; - public int MuteTime { get; set; } - public ulong? RoleId { get; set; } - public HashSet IgnoredChannels { get; set; } = new(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/club/ClubInfo.cs b/src/Ellie.Bot.Db/Models/club/ClubInfo.cs deleted file mode 100644 index 6cefb77..0000000 --- a/src/Ellie.Bot.Db/Models/club/ClubInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; -using System.ComponentModel.DataAnnotations; - -namespace Ellie.Db.Models; - -public class ClubInfo : DbEntity -{ - [MaxLength(20)] - public string Name { get; set; } - public string Description { get; set; } - public string ImageUrl { get; set; } = string.Empty; - - public int Xp { get; set; } = 0; - public int? OwnerId { get; set; } - public DiscordUser Owner { get; set; } - - public List Members { get; set; } = new(); - public List Applicants { get; set; } = new(); - public List Bans { get; set; } = new(); - - public override string ToString() - => Name; -} - -public class ClubApplicants -{ - public int ClubId { get; set; } - public ClubInfo Club { get; set; } - - public int UserId { get; set; } - public DiscordUser User { get; set; } -} - -public class ClubBans -{ - public int ClubId { get; set; } - public ClubInfo Club { get; set; } - - public int UserId { get; set; } - public DiscordUser User { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/currency/BankUser.cs b/src/Ellie.Bot.Db/Models/currency/BankUser.cs deleted file mode 100644 index 425b4d7..0000000 --- a/src/Ellie.Bot.Db/Models/currency/BankUser.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Db.Models; - -public class BankUser : DbEntity -{ - public ulong UserId { get; set; } - public long Balance { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/expr/EllieExpression.cs b/src/Ellie.Bot.Db/Models/expr/EllieExpression.cs deleted file mode 100644 index 49ad645..0000000 --- a/src/Ellie.Bot.Db/Models/expr/EllieExpression.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class EllieExpression : DbEntity -{ - public ulong? GuildId { get; set; } - public string Response { get; set; } - public string Trigger { get; set; } - - public bool AutoDeleteTrigger { get; set; } - public bool DmResponse { get; set; } - public bool ContainsAnywhere { get; set; } - public bool AllowTarget { get; set; } - public string Reactions { get; set; } - - public string[] GetReactions() - => string.IsNullOrWhiteSpace(Reactions) ? Array.Empty() : Reactions.Split("@@@"); - - public bool IsGlobal() - => GuildId is null or 0; -} - -public class ReactionResponse : DbEntity -{ - public bool OwnerOnly { get; set; } - public string Text { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/expr/Quote.cs b/src/Ellie.Bot.Db/Models/expr/Quote.cs deleted file mode 100644 index 2cc0c1a..0000000 --- a/src/Ellie.Bot.Db/Models/expr/Quote.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -using System.ComponentModel.DataAnnotations; - -namespace Ellie.Services.Database.Models; - - -public class Quote : DbEntity -{ - public ulong GuildId { get; set; } - - [Required] - public string Keyword { get; set; } - - [Required] - public string AuthorName { get; set; } - - public ulong AuthorId { get; set; } - - [Required] - public string Text { get; set; } -} - -public enum OrderType -{ - Id = -1, - Keyword = -2 -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/filter/FilterChannelId.cs b/src/Ellie.Bot.Db/Models/filter/FilterChannelId.cs deleted file mode 100644 index 7b64da6..0000000 --- a/src/Ellie.Bot.Db/Models/filter/FilterChannelId.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class FilterChannelId : DbEntity -{ - public ulong ChannelId { get; set; } - - public bool Equals(FilterChannelId other) - => ChannelId == other.ChannelId; - - public override bool Equals(object obj) - => obj is FilterChannelId fci && Equals(fci); - - public override int GetHashCode() - => ChannelId.GetHashCode(); -} - -public class FilterWordsChannelId : DbEntity -{ - public ulong ChannelId { get; set; } - - public bool Equals(FilterWordsChannelId other) - => ChannelId == other.ChannelId; - - public override bool Equals(object obj) - => obj is FilterWordsChannelId fci && Equals(fci); - - public override int GetHashCode() - => ChannelId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/filter/FilterLinksChannelId.cs b/src/Ellie.Bot.Db/Models/filter/FilterLinksChannelId.cs deleted file mode 100644 index e044ddf..0000000 --- a/src/Ellie.Bot.Db/Models/filter/FilterLinksChannelId.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class FilterLinksChannelId : DbEntity -{ - public ulong ChannelId { get; set; } - - public override bool Equals(object obj) - => obj is FilterLinksChannelId f && f.ChannelId == ChannelId; - - public override int GetHashCode() - => ChannelId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/filter/FilteredWord.cs b/src/Ellie.Bot.Db/Models/filter/FilteredWord.cs deleted file mode 100644 index 28bace4..0000000 --- a/src/Ellie.Bot.Db/Models/filter/FilteredWord.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class FilteredWord : DbEntity -{ - public string Word { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/BanTemplate.cs b/src/Ellie.Bot.Db/Models/punish/BanTemplate.cs deleted file mode 100644 index 2ccdb4c..0000000 --- a/src/Ellie.Bot.Db/Models/punish/BanTemplate.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class BanTemplate : DbEntity -{ - public ulong GuildId { get; set; } - public string Text { get; set; } - public int? PruneDays { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/MutedUserId.cs b/src/Ellie.Bot.Db/Models/punish/MutedUserId.cs deleted file mode 100644 index 23f703d..0000000 --- a/src/Ellie.Bot.Db/Models/punish/MutedUserId.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class MutedUserId : DbEntity -{ - public ulong UserId { get; set; } - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override bool Equals(object obj) - => obj is MutedUserId mui ? mui.UserId == UserId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/PunishmentAction.cs b/src/Ellie.Bot.Db/Models/punish/PunishmentAction.cs deleted file mode 100644 index 67f41aa..0000000 --- a/src/Ellie.Bot.Db/Models/punish/PunishmentAction.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Ellie.Services.Database.Models; - -public enum PunishmentAction -{ - Mute, - Kick, - Ban, - Softban, - RemoveRoles, - ChatMute, - VoiceMute, - AddRole, - Warn, - TimeOut -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/WarnExpireAction.cs b/src/Ellie.Bot.Db/Models/punish/WarnExpireAction.cs deleted file mode 100644 index 7937ed3..0000000 --- a/src/Ellie.Bot.Db/Models/punish/WarnExpireAction.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public enum WarnExpireAction -{ - Clear, - Delete -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/Warning.cs b/src/Ellie.Bot.Db/Models/punish/Warning.cs deleted file mode 100644 index b41e4ed..0000000 --- a/src/Ellie.Bot.Db/Models/punish/Warning.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class Warning : DbEntity -{ - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - public string Reason { get; set; } - public bool Forgiven { get; set; } - public string ForgivenBy { get; set; } - public string Moderator { get; set; } - public long Weight { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/punish/WarningPunishment.cs b/src/Ellie.Bot.Db/Models/punish/WarningPunishment.cs deleted file mode 100644 index a4242fd..0000000 --- a/src/Ellie.Bot.Db/Models/punish/WarningPunishment.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class WarningPunishment : DbEntity -{ - public int Count { get; set; } - public PunishmentAction Punishment { get; set; } - public int Time { get; set; } - public ulong? RoleId { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredRole.cs b/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredRole.cs deleted file mode 100644 index 011eacd..0000000 --- a/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredRole.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class SlowmodeIgnoredRole : DbEntity -{ - public ulong RoleId { get; set; } - - // override object.Equals - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - return false; - - return ((SlowmodeIgnoredRole)obj).RoleId == RoleId; - } - - // override object.GetHashCode - public override int GetHashCode() - => RoleId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredUser.cs b/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredUser.cs deleted file mode 100644 index 0cbb143..0000000 --- a/src/Ellie.Bot.Db/Models/slowmode/SlowmodeIgnoredUser.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class SlowmodeIgnoredUser : DbEntity -{ - public ulong UserId { get; set; } - - // override object.Equals - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - return false; - - return ((SlowmodeIgnoredUser)obj).UserId == UserId; - } - - // override object.GetHashCode - public override int GetHashCode() - => UserId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/support/PatronQuota.cs b/src/Ellie.Bot.Db/Models/support/PatronQuota.cs deleted file mode 100644 index afbb5bb..0000000 --- a/src/Ellie.Bot.Db/Models/support/PatronQuota.cs +++ /dev/null @@ -1,48 +0,0 @@ -#nullable disable -namespace Ellie.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; } - - public PatronUser Clone() - => new PatronUser() - { - UniquePlatformUserId = this.UniquePlatformUserId, - UserId = this.UserId, - AmountCents = this.AmountCents, - LastCharge = this.LastCharge, - ValidThru = this.ValidThru - }; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/support/RewardUser.cs b/src/Ellie.Bot.Db/Models/support/RewardUser.cs deleted file mode 100644 index b95e700..0000000 --- a/src/Ellie.Bot.Db/Models/support/RewardUser.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class RewardedUser : DbEntity -{ - public ulong UserId { 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/Ellie.Bot.Db/Models/untimer/UnbanTimer.cs b/src/Ellie.Bot.Db/Models/untimer/UnbanTimer.cs deleted file mode 100644 index 86c12b8..0000000 --- a/src/Ellie.Bot.Db/Models/untimer/UnbanTimer.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class UnbanTimer : DbEntity -{ - public ulong UserId { get; set; } - public DateTime UnbanAt { get; set; } - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override bool Equals(object obj) - => obj is UnbanTimer ut ? ut.UserId == UserId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/untimer/UnmuteTimer.cs b/src/Ellie.Bot.Db/Models/untimer/UnmuteTimer.cs deleted file mode 100644 index 16e88a9..0000000 --- a/src/Ellie.Bot.Db/Models/untimer/UnmuteTimer.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class UnmuteTimer : DbEntity -{ - public ulong UserId { get; set; } - public DateTime UnmuteAt { get; set; } - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override bool Equals(object obj) - => obj is UnmuteTimer ut ? ut.UserId == UserId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/untimer/UnroleTimer.cs b/src/Ellie.Bot.Db/Models/untimer/UnroleTimer.cs deleted file mode 100644 index 92ff05f..0000000 --- a/src/Ellie.Bot.Db/Models/untimer/UnroleTimer.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class UnroleTimer : DbEntity -{ - public ulong UserId { get; set; } - public ulong RoleId { get; set; } - public DateTime UnbanAt { get; set; } - - public override int GetHashCode() - => UserId.GetHashCode() ^ RoleId.GetHashCode(); - - public override bool Equals(object obj) - => obj is UnroleTimer ut ? ut.UserId == UserId && ut.RoleId == RoleId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/xp/UserXpStats.cs b/src/Ellie.Bot.Db/Models/xp/UserXpStats.cs deleted file mode 100644 index fe9bd31..0000000 --- a/src/Ellie.Bot.Db/Models/xp/UserXpStats.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class UserXpStats : DbEntity -{ - public ulong UserId { get; set; } - public ulong GuildId { get; set; } - public long Xp { get; set; } - public long AwardedXp { get; set; } - public XpNotificationLocation NotifyOnLevelUp { get; set; } -} - -public enum XpNotificationLocation { None, Dm, Channel } \ No newline at end of file diff --git a/src/Ellie.Bot.Db/Models/xp/XpSettings.cs b/src/Ellie.Bot.Db/Models/xp/XpSettings.cs deleted file mode 100644 index c2b4346..0000000 --- a/src/Ellie.Bot.Db/Models/xp/XpSettings.cs +++ /dev/null @@ -1,62 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class XpSettings : DbEntity -{ - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public HashSet RoleRewards { get; set; } = new(); - public HashSet CurrencyRewards { get; set; } = new(); - public HashSet ExclusionList { get; set; } = new(); - public bool ServerExcluded { get; set; } -} - -public enum ExcludedItemType { Channel, Role } - -public class XpRoleReward : DbEntity -{ - public int XpSettingsId { get; set; } - public XpSettings XpSettings { get; set; } - - public int Level { get; set; } - public ulong RoleId { get; set; } - - /// - /// Whether the role should be removed (true) or added (false) - /// - public bool Remove { get; set; } - - public override int GetHashCode() - => Level.GetHashCode() ^ XpSettingsId.GetHashCode(); - - public override bool Equals(object obj) - => obj is XpRoleReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; -} - -public class XpCurrencyReward : DbEntity -{ - public int XpSettingsId { get; set; } - public XpSettings XpSettings { get; set; } - - public int Level { get; set; } - public int Amount { get; set; } - - public override int GetHashCode() - => Level.GetHashCode() ^ XpSettingsId.GetHashCode(); - - public override bool Equals(object obj) - => obj is XpCurrencyReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; -} - -public class ExcludedItem : DbEntity -{ - public ulong ItemId { get; set; } - public ExcludedItemType ItemType { get; set; } - - public override int GetHashCode() - => ItemId.GetHashCode() ^ ItemType.GetHashCode(); - - public override bool Equals(object obj) - => obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs b/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs deleted file mode 100644 index 45392d1..0000000 --- a/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs +++ /dev/null @@ -1,258 +0,0 @@ -// Code temporarily yeeted from -// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs -// because of NRT issue -#nullable enable -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using System.Text; - -namespace Cloneable -{ - [Generator] - public class CloneableGenerator : ISourceGenerator - { - private const string PREVENT_DEEP_COPY_KEY_STRING = "PreventDeepCopy"; - private const string EXPLICIT_DECLARATION_KEY_STRING = "ExplicitDeclaration"; - - private const string CLONEABLE_NAMESPACE = "Cloneable"; - private const string CLONEABLE_ATTRIBUTE_STRING = "CloneableAttribute"; - private const string CLONE_ATTRIBUTE_STRING = "CloneAttribute"; - private const string IGNORE_CLONE_ATTRIBUTE_STRING = "IgnoreCloneAttribute"; - - private const string CLONEABLE_ATTRIBUTE_TEXT = $$""" - // - using System; - - namespace {{CLONEABLE_NAMESPACE}} - { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)] - internal sealed class {{CLONEABLE_ATTRIBUTE_STRING}} : Attribute - { - public {{CLONEABLE_ATTRIBUTE_STRING}}() - { - } - - public bool {{EXPLICIT_DECLARATION_KEY_STRING}} { get; set; } - } - } - - """; - - private const string CLONE_PROPERTY_ATTRIBUTE_TEXT = $$""" - // - using System; - - namespace {{CLONEABLE_NAMESPACE}} - { - [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] - internal sealed class {{CLONE_ATTRIBUTE_STRING}} : Attribute - { - public {{CLONE_ATTRIBUTE_STRING}}() - { - } - - public bool {{PREVENT_DEEP_COPY_KEY_STRING}} { get; set; } - } - } - - """; - - private const string IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT = $$""" - // - using System; - - namespace {{CLONEABLE_NAMESPACE}} - { - [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] - internal sealed class {{IGNORE_CLONE_ATTRIBUTE_STRING}} : Attribute - { - public {{IGNORE_CLONE_ATTRIBUTE_STRING}}() - { - } - } - } - - """; - - private INamedTypeSymbol? _cloneableAttribute; - private INamedTypeSymbol? _ignoreCloneAttribute; - private INamedTypeSymbol? _cloneAttribute; - - public void Initialize(GeneratorInitializationContext context) - => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - - public void Execute(GeneratorExecutionContext context) - { - InjectCloneableAttributes(context); - GenerateCloneMethods(context); - } - - private void GenerateCloneMethods(GeneratorExecutionContext context) - { - if (context.SyntaxReceiver is not SyntaxReceiver receiver) - return; - - Compilation compilation = GetCompilation(context); - - InitAttributes(compilation); - - var classSymbols = GetClassSymbols(compilation, receiver); - foreach (var classSymbol in classSymbols) - { - if (!classSymbol.TryGetAttribute(_cloneableAttribute!, out var attributes)) - continue; - - var attribute = attributes.Single(); - var isExplicit = (bool?)attribute.NamedArguments.FirstOrDefault(e => e.Key.Equals(EXPLICIT_DECLARATION_KEY_STRING)).Value.Value ?? false; - context.AddSource($"{classSymbol.Name}_cloneable.g.cs", SourceText.From(CreateCloneableCode(classSymbol, isExplicit), Encoding.UTF8)); - } - } - - private void InitAttributes(Compilation compilation) - { - _cloneableAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONEABLE_ATTRIBUTE_STRING}")!; - _cloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONE_ATTRIBUTE_STRING}")!; - _ignoreCloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{IGNORE_CLONE_ATTRIBUTE_STRING}")!; - } - - private static Compilation GetCompilation(GeneratorExecutionContext context) - { - var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; - - var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8), options)). - AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)). - AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)); - return compilation; - } - - private string CreateCloneableCode(INamedTypeSymbol classSymbol, bool isExplicit) - { - string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); - var fieldAssignmentsCode = GenerateFieldAssignmentsCode(classSymbol, isExplicit).ToList(); - var fieldAssignmentsCodeSafe = fieldAssignmentsCode.Select(x => - { - if (x.isCloneable) - return x.line + "Safe(referenceChain)"; - return x.line; - }); - var fieldAssignmentsCodeFast = fieldAssignmentsCode.Select(x => - { - if (x.isCloneable) - return x.line + "()"; - return x.line; - }); - - return $@"using System.Collections.Generic; - -namespace {namespaceName} -{{ - {GetAccessModifier(classSymbol)} partial class {classSymbol.Name} - {{ - /// - /// Creates a copy of {classSymbol.Name} with NO circular reference checking. This method should be used if performance matters. - /// - /// Will occur on any object that has circular references in the hierarchy. - /// - public {classSymbol.Name} Clone() - {{ - return new {classSymbol.Name} - {{ -{string.Join(",\n", fieldAssignmentsCodeFast)} - }}; - }} - - /// - /// Creates a copy of {classSymbol.Name} with circular reference checking. If a circular reference was detected, only a reference of the leaf object is passed instead of cloning it. - /// - /// Should only be provided if specific objects should not be cloned but passed by reference instead. - public {classSymbol.Name} CloneSafe(Stack referenceChain = null) - {{ - if(referenceChain?.Contains(this) == true) - return this; - referenceChain ??= new Stack(); - referenceChain.Push(this); - var result = new {classSymbol.Name} - {{ -{string.Join($",\n", fieldAssignmentsCodeSafe)} - }}; - referenceChain.Pop(); - return result; - }} - }} -}}"; - } - - private IEnumerable<(string line, bool isCloneable)> GenerateFieldAssignmentsCode(INamedTypeSymbol classSymbol, bool isExplicit ) - { - var fieldNames = GetCloneableProperties(classSymbol, isExplicit); - - var fieldAssignments = fieldNames.Select(field => IsFieldCloneable(field, classSymbol)) - .OrderBy(x => x.isCloneable) - .Select(x => (GenerateAssignmentCode(x.item.Name, x.isCloneable), x.isCloneable)); - return fieldAssignments; - } - - private string GenerateAssignmentCode(string name, bool isCloneable) - { - if (isCloneable) - { - return $@" {name} = this.{name}?.Clone"; - } - - return $@" {name} = this.{name}"; - } - - private (IPropertySymbol item, bool isCloneable) IsFieldCloneable(IPropertySymbol x, INamedTypeSymbol classSymbol) - { - if (SymbolEqualityComparer.Default.Equals(x.Type, classSymbol)) - { - return (x, false); - } - - if (!x.Type.TryGetAttribute(_cloneableAttribute!, out var attributes)) - { - return (x, false); - } - - var preventDeepCopy = (bool?)attributes.Single().NamedArguments.FirstOrDefault(e => e.Key.Equals(PREVENT_DEEP_COPY_KEY_STRING)).Value.Value ?? false; - return (item: x, !preventDeepCopy); - } - - private string GetAccessModifier(INamedTypeSymbol classSymbol) - => classSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(); - - private IEnumerable GetCloneableProperties(ITypeSymbol classSymbol, bool isExplicit) - { - var targetSymbolMembers = classSymbol.GetMembers().OfType() - .Where(x => x.SetMethod is not null && - x.CanBeReferencedByName); - if (isExplicit) - { - return targetSymbolMembers.Where(x => x.HasAttribute(_cloneAttribute!)); - } - else - { - return targetSymbolMembers.Where(x => !x.HasAttribute(_ignoreCloneAttribute!)); - } - } - - private static IEnumerable GetClassSymbols(Compilation compilation, SyntaxReceiver receiver) - => receiver.CandidateClasses.Select(clazz => GetClassSymbol(compilation, clazz)); - - private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax clazz) - { - var model = compilation.GetSemanticModel(clazz.SyntaxTree); - var classSymbol = model.GetDeclaredSymbol(clazz)!; - return classSymbol; - } - - private static void InjectCloneableAttributes(GeneratorExecutionContext context) - { - context.AddSource(CLONEABLE_ATTRIBUTE_STRING, SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8)); - context.AddSource(CLONE_ATTRIBUTE_STRING, SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8)); - context.AddSource(IGNORE_CLONE_ATTRIBUTE_STRING, SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs b/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs deleted file mode 100644 index d009da3..0000000 --- a/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Code temporarily yeeted from -// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs -// because of NRT issue - -using Microsoft.CodeAnalysis; - -namespace Cloneable -{ - internal static class SymbolExtensions - { - public static bool TryGetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType, - out IEnumerable attributes) - { - attributes = symbol.GetAttributes() - .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); - return attributes.Any(); - } - - public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) - => symbol.GetAttributes() - .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Generators.Cloneable/SyntaxReceiver.cs b/src/Ellie.Bot.Generators.Cloneable/SyntaxReceiver.cs deleted file mode 100644 index 00931bb..0000000 --- a/src/Ellie.Bot.Generators.Cloneable/SyntaxReceiver.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Code temporarily yeeted from -// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs -// because of NRT issue - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Cloneable -{ - internal class SyntaxReceiver : ISyntaxReceiver - { - public IList CandidateClasses { get; } = new List(); - - /// - /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation - /// - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - // any field with at least one attribute is a candidate for being cloneable - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && - classDeclarationSyntax.AttributeLists.Count > 0) - { - CandidateClasses.Add(classDeclarationSyntax); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Generators.Strings/LocalizedStringsGenerator.cs b/src/Ellie.Bot.Generators.Strings/LocalizedStringsGenerator.cs deleted file mode 100644 index 0bf2875..0000000 --- a/src/Ellie.Bot.Generators.Strings/LocalizedStringsGenerator.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable enable -using System.CodeDom.Compiler; -using System.Diagnostics; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Newtonsoft.Json; - -namespace Ellie.Generators -{ - internal readonly struct TranslationPair - { - public string Name { get; } - public string Value { get; } - - public TranslationPair(string name, string value) - { - Name = name; - Value = value; - } - } - - [Generator] - public class LocalizedStringsGenerator : ISourceGenerator - { - // private const string LOC_STR_SOURCE = @"namespace Ellie - // { - // public readonly struct LocStr - // { - // public readonly string Key; - // public readonly object[] Params; - // - // public LocStr(string key, params object[] data) - // { - // Key = key; - // Params = data; - // } - // } - // }"; - - public void Initialize(GeneratorInitializationContext context) - { - } - - public void Execute(GeneratorExecutionContext context) - { - var file = context.AdditionalFiles.First(x => x.Path.EndsWith("responses.en-US.json")); - - var fields = GetFields(file.GetText()?.ToString()); - - using (var stringWriter = new StringWriter()) - using (var sw = new IndentedTextWriter(stringWriter)) - { - sw.WriteLine("#pragma warning disable CS8981"); - sw.WriteLine("namespace Ellie;"); - sw.WriteLine(); - - sw.WriteLine("public static class strs"); - sw.WriteLine("{"); - sw.Indent++; - - var typedParamStrings = new List(10); - foreach (var field in fields) - { - var matches = Regex.Matches(field.Value, @"{(?\d)[}:]"); - var max = 0; - foreach (Match match in matches) - { - max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1); - } - - typedParamStrings.Clear(); - var typeParams = new string[max]; - var passedParamString = string.Empty; - for (var i = 0; i < max; i++) - { - typedParamStrings.Add($"in T{i} p{i}"); - passedParamString += $", p{i}"; - typeParams[i] = $"T{i}"; - } - - var sig = string.Empty; - var typeParamStr = string.Empty; - if (max > 0) - { - sig = $"({string.Join(", ", typedParamStrings)})"; - typeParamStr = $"<{string.Join(", ", typeParams)}>"; - } - - sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});", - field.Name, - typeParamStr, - sig, - field.Name, - passedParamString); - } - - sw.Indent--; - sw.WriteLine("}"); - - - sw.Flush(); - context.AddSource("strs.g.cs", stringWriter.ToString()); - } - - // context.AddSource("LocStr.g.cs", LOC_STR_SOURCE); - } - - private List GetFields(string? dataText) - { - if (string.IsNullOrWhiteSpace(dataText)) - return new(); - - Dictionary data; - try - { - var output = JsonConvert.DeserializeObject>(dataText!); - if (output is null) - return new(); - - data = output; - } - catch - { - Debug.WriteLine("Failed parsing responses file."); - return new(); - } - - var list = new List(); - foreach (var entry in data) - { - list.Add(new( - entry.Key, - entry.Value - )); - } - - return list; - } - } -} diff --git a/src/Ellie.Bot.Modules.Administration/Administration.cs b/src/Ellie.Bot.Modules.Administration/Administration.cs deleted file mode 100644 index 5a4cc16..0000000 --- a/src/Ellie.Bot.Modules.Administration/Administration.cs +++ /dev/null @@ -1,405 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders.Models; -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration : EllieModule -{ - public enum Channel - { - Channel, - Ch, - Chnl, - Chan - } - - public enum List - { - List = 0, - Ls = 0 - } - - public enum Server - { - Server - } - - public enum State - { - Enable, - Disable, - Inherit - } - - private readonly SomethingOnlyChannelService _somethingOnly; - private readonly AutoPublishService _autoPubService; - - public Administration(SomethingOnlyChannelService somethingOnly, AutoPublishService autoPubService) - { - _somethingOnly = somethingOnly; - _autoPubService = autoPubService; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageGuild)] - public async Task ImageOnlyChannel(StoopidTime time = null) - { - var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.imageonly_enable); - else - await ReplyPendingLocalizedAsync(strs.imageonly_disable); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageGuild)] - public async Task LinkOnlyChannel(StoopidTime time = null) - { - var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.linkonly_enable); - else - await ReplyPendingLocalizedAsync(strs.linkonly_disable); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageChannels)] - [BotPerm(ChannelPerm.ManageChannels)] - public async Task Slowmode(StoopidTime time = null) - { - var seconds = (int?)time?.Time.TotalSeconds ?? 0; - if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6))) - return; - - await ((ITextChannel)ctx.Channel).ModifyAsync(tcp => - { - tcp.SlowModeInterval = seconds; - }); - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(2)] - public async Task Delmsgoncmd(List _) - { - var guild = (SocketGuild)ctx.Guild; - var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id); - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.server_delmsgoncmd)) - .WithDescription(enabled ? "✅" : "❌"); - - var str = string.Join("\n", - channels.Select(x => - { - var ch = guild.GetChannel(x.ChannelId)?.ToString() ?? x.ChannelId.ToString(); - var prefixSign = x.State ? "✅ " : "❌ "; - return prefixSign + ch; - })); - - if (string.IsNullOrWhiteSpace(str)) - str = "-"; - - embed.AddField(GetText(strs.channel_delmsgoncmd), str); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(1)] - public async Task Delmsgoncmd(Server _ = Server.Server) - { - if (_service.ToggleDeleteMessageOnCommand(ctx.Guild.Id)) - { - _service.DeleteMessagesOnCommand.Add(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.delmsg_on); - } - else - { - _service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.delmsg_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(0)] - public Task Delmsgoncmd(Channel _, State s, ITextChannel ch) - => Delmsgoncmd(_, s, ch.Id); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(1)] - public async Task Delmsgoncmd(Channel _, State s, ulong? chId = null) - { - var actualChId = chId ?? ctx.Channel.Id; - await _service.SetDelMsgOnCmdState(ctx.Guild.Id, actualChId, s); - - if (s == State.Disable) - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_off); - else if (s == State.Enable) - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_on); - else - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_inherit); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.DeafenMembers)] - [BotPerm(GuildPerm.DeafenMembers)] - public async Task Deafen(params IGuildUser[] users) - { - await _service.DeafenUsers(true, users); - await ReplyConfirmLocalizedAsync(strs.deafen); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.DeafenMembers)] - [BotPerm(GuildPerm.DeafenMembers)] - public async Task UnDeafen(params IGuildUser[] users) - { - await _service.DeafenUsers(false, users); - await ReplyConfirmLocalizedAsync(strs.undeafen); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel) - { - await voiceChannel.DeleteAsync(); - await ReplyConfirmLocalizedAsync(strs.delvoich(Format.Bold(voiceChannel.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task CreatVoiChanl([Leftover] string channelName) - { - var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName); - await ReplyConfirmLocalizedAsync(strs.createvoich(Format.Bold(ch.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task DelTxtChanl([Leftover] ITextChannel toDelete) - { - await toDelete.DeleteAsync(); - await ReplyConfirmLocalizedAsync(strs.deltextchan(Format.Bold(toDelete.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task CreaTxtChanl([Leftover] string channelName) - { - var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName); - await ReplyConfirmLocalizedAsync(strs.createtextchan(Format.Bold(txtCh.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task SetTopic([Leftover] string topic = null) - { - var channel = (ITextChannel)ctx.Channel; - topic ??= ""; - await channel.ModifyAsync(c => c.Topic = topic); - await ReplyConfirmLocalizedAsync(strs.set_topic); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task SetChanlName([Leftover] string name) - { - var channel = (ITextChannel)ctx.Channel; - await channel.ModifyAsync(c => c.Name = name); - await ReplyConfirmLocalizedAsync(strs.set_channel_name); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task NsfwToggle() - { - var channel = (ITextChannel)ctx.Channel; - var isEnabled = channel.IsNsfw; - - await channel.ModifyAsync(c => c.IsNsfw = !isEnabled); - - if (isEnabled) - await ReplyConfirmLocalizedAsync(strs.nsfw_set_false); - else - await ReplyConfirmLocalizedAsync(strs.nsfw_set_true); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [Priority(0)] - public Task Edit(ulong messageId, [Leftover] string text) - => Edit((ITextChannel)ctx.Channel, messageId, text); - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task Edit(ITextChannel channel, ulong messageId, [Leftover] string text) - { - var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel); - var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel); - if (!userPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u); - return; - } - - if (!botPerms.Has(ChannelPermission.ViewChannel)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i); - return; - } - - await _service.EditMessage(ctx, channel, messageId, text); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - public Task Delete(ulong messageId, StoopidTime time = null) - => Delete((ITextChannel)ctx.Channel, messageId, time); - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null) - => await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync()); - - private async Task InternalMessageAction( - ITextChannel channel, - ulong messageId, - StoopidTime time, - Func func) - { - var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel); - var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel); - if (!userPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u); - return; - } - - if (!botPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i); - return; - } - - - var msg = await channel.GetMessageAsync(messageId); - if (msg is null) - { - await ReplyErrorLocalizedAsync(strs.msg_not_found); - return; - } - - if (time is null) - await msg.DeleteAsync(); - else if (time.Time <= TimeSpan.FromDays(7)) - { - _ = Task.Run(async () => - { - await Task.Delay(time.Time); - await msg.DeleteAsync(); - }); - } - else - { - await ReplyErrorLocalizedAsync(strs.time_too_long); - return; - } - - await ctx.OkAsync(); - } - - [Cmd] - [BotPerm(ChannelPermission.CreatePublicThreads)] - [UserPerm(ChannelPermission.CreatePublicThreads)] - public async Task ThreadCreate([Leftover] string name) - { - if (ctx.Channel is not SocketTextChannel stc) - return; - - await stc.CreateThreadAsync(name, message: ctx.Message.ReferencedMessage); - await ctx.OkAsync(); - } - - [Cmd] - [BotPerm(ChannelPermission.ManageThreads)] - [UserPerm(ChannelPermission.ManageThreads)] - public async Task ThreadDelete([Leftover] string name) - { - if (ctx.Channel is not SocketTextChannel stc) - return; - - var t = stc.Threads.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase)); - - if (t is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - await t.DeleteAsync(); - await ctx.OkAsync(); - } - - [Cmd] - [UserPerm(ChannelPerm.ManageMessages)] - public async Task AutoPublish() - { - if (ctx.Channel.GetChannelType() != ChannelType.News) - { - await ReplyErrorLocalizedAsync(strs.req_announcement_channel); - return; - } - - var newState = await _autoPubService.ToggleAutoPublish(ctx.Guild.Id, ctx.Channel.Id); - - if (newState) - { - await ReplyConfirmLocalizedAsync(strs.autopublish_enable); - } - else - { - await ReplyConfirmLocalizedAsync(strs.autopublish_disable); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/AdministrationService.cs b/src/Ellie.Bot.Modules.Administration/AdministrationService.cs deleted file mode 100644 index 128dad5..0000000 --- a/src/Ellie.Bot.Modules.Administration/AdministrationService.cs +++ /dev/null @@ -1,158 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public class AdministrationService : IEService -{ - public ConcurrentHashSet DeleteMessagesOnCommand { get; } - public ConcurrentDictionary DeleteMessagesOnCommandChannels { get; } - - private readonly DbService _db; - private readonly ILogCommandService _logService; - - public AdministrationService( - IBot bot, - CommandHandler cmdHandler, - DbService db, - ILogCommandService logService) - { - _db = db; - _logService = logService; - - DeleteMessagesOnCommand = new(bot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId)); - - DeleteMessagesOnCommandChannels = new(bot.AllGuildConfigs.SelectMany(x => x.DelMsgOnCmdChannels) - .ToDictionary(x => x.ChannelId, x => x.State) - .ToConcurrent()); - - cmdHandler.CommandExecuted += DelMsgOnCmd_Handler; - } - - public (bool DelMsgOnCmd, IEnumerable channels) GetDelMsgOnCmdData(ulong guildId) - { - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels)); - - return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels); - } - - private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd) - { - if (msg.Channel is not ITextChannel channel) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - //wat ?! - if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state)) - { - if (state && cmd.Name != "prune" && cmd.Name != "pick") - { - _logService.AddDeleteIgnore(msg.Id); - try { await msg.DeleteAsync(); } - catch { } - } - //if state is false, that means do not do it - } - else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick") - { - _logService.AddDeleteIgnore(msg.Id); - try { await msg.DeleteAsync(); } - catch { } - } - }); - return Task.CompletedTask; - } - - public bool ToggleDeleteMessageOnCommand(ulong guildId) - { - bool enabled; - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand; - - uow.SaveChanges(); - return enabled; - } - - public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState) - { - await using (var uow = _db.GetDbContext()) - { - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels)); - - var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId); - if (newState == Administration.State.Inherit) - { - if (old is not null) - { - conf.DelMsgOnCmdChannels.Remove(old); - uow.Remove(old); - } - } - else - { - if (old is null) - { - old = new() - { - ChannelId = chId - }; - conf.DelMsgOnCmdChannels.Add(old); - } - - old.State = newState == Administration.State.Enable; - DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable; - } - - await uow.SaveChangesAsync(); - } - - if (newState == Administration.State.Disable) - { - } - else if (newState == Administration.State.Enable) - DeleteMessagesOnCommandChannels[chId] = true; - else - DeleteMessagesOnCommandChannels.TryRemove(chId, out _); - } - - public async Task DeafenUsers(bool value, params IGuildUser[] users) - { - if (!users.Any()) - return; - foreach (var u in users) - { - try - { - await u.ModifyAsync(usr => usr.Deaf = value); - } - catch - { - // ignored - } - } - } - - public async Task EditMessage( - ICommandContext context, - ITextChannel chanl, - ulong messageId, - string input) - { - var msg = await chanl.GetMessageAsync(messageId); - - if (msg is not IUserMessage umsg || msg.Author.Id != context.Client.CurrentUser.Id) - return; - - var rep = new ReplacementBuilder().WithDefault(context).Build(); - - var text = SmartText.CreateFrom(input); - text = rep.Replace(text); - - await umsg.EditAsync(text); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleCommands.cs b/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleCommands.cs deleted file mode 100644 index 57fda4d..0000000 --- a/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleCommands.cs +++ /dev/null @@ -1,58 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class AutoAssignRoleCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task AutoAssignRole([Leftover] IRole role) - { - var guser = (IGuildUser)ctx.User; - if (role.Id == ctx.Guild.EveryoneRole.Id) - return; - - // the user can't aar the role which is higher or equal to his highest role - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - { - await ReplyErrorLocalizedAsync(strs.hierarchy); - return; - } - - var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id); - if (roles.Count == 0) - await ReplyConfirmLocalizedAsync(strs.aar_disabled); - else if (roles.Contains(role.Id)) - await AutoAssignRole(); - else - await ReplyConfirmLocalizedAsync(strs.aar_role_removed(Format.Bold(role.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task AutoAssignRole() - { - if (!_service.TryGetRoles(ctx.Guild.Id, out var roles)) - { - await ReplyConfirmLocalizedAsync(strs.aar_none); - return; - } - - var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => r is not null).ToList(); - - if (existing.Count != roles.Count) - await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id)); - - await ReplyConfirmLocalizedAsync(strs.aar_roles( - '\n' + existing.Select(x => Format.Bold(x.ToString())).Join(",\n"))); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleService.cs b/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleService.cs deleted file mode 100644 index 3435815..0000000 --- a/src/Ellie.Bot.Modules.Administration/AutoAssignableRoles/AutoAssignRoleService.cs +++ /dev/null @@ -1,159 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; -using System.Net; -using System.Threading.Channels; -using LinqToDB; -using Microsoft.EntityFrameworkCore; -using Ellie.Db; - -namespace Ellie.Modules.Administration.Services; - -public sealed class AutoAssignRoleService : IEService -{ - private readonly DiscordSocketClient _client; - private readonly DbService _db; - - //guildid/roleid - private readonly ConcurrentDictionary> _autoAssignableRoles; - - private readonly Channel _assignQueue = Channel.CreateBounded( - new BoundedChannelOptions(100) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = true, - SingleWriter = false - }); - - public AutoAssignRoleService(DiscordSocketClient client, IBot bot, DbService db) - { - _client = client; - _db = db; - - _autoAssignableRoles = bot.AllGuildConfigs.Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds)) - .ToDictionary>(k => k.GuildId, - v => v.GetAutoAssignableRoles()) - .ToConcurrent(); - - _ = Task.Run(async () => - { - while (true) - { - var user = await _assignQueue.Reader.ReadAsync(); - if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds)) - continue; - - try - { - var roleIds = savedRoleIds.Select(roleId => user.Guild.GetRole(roleId)) - .Where(x => x is not null) - .ToList(); - - if (roleIds.Any()) - { - await user.AddRolesAsync(roleIds); - await Task.Delay(250); - } - else - { - Log.Warning( - "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist", - user.Guild.Name, - user.Guild.Id); - - await DisableAarAsync(user.Guild.Id); - } - } - catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) - { - Log.Warning( - "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions", - user.Guild.Name, - user.Guild.Id); - - await DisableAarAsync(user.Guild.Id); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist"); - } - } - }); - - _client.UserJoined += OnClientOnUserJoined; - _client.RoleDeleted += OnClientRoleDeleted; - } - - private async Task OnClientRoleDeleted(SocketRole role) - { - if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles) && roles.Contains(role.Id)) - await ToggleAarAsync(role.Guild.Id, role.Id); - } - - private async Task OnClientOnUserJoined(SocketGuildUser user) - { - if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _)) - await _assignQueue.Writer.WriteAsync(user); - } - - public async Task> ToggleAarAsync(ulong guildId, ulong roleId) - { - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - var roles = gc.GetAutoAssignableRoles(); - if (!roles.Remove(roleId) && roles.Count < 3) - roles.Add(roleId); - - gc.SetAutoAssignableRoles(roles); - await uow.SaveChangesAsync(); - - if (roles.Count > 0) - _autoAssignableRoles[guildId] = roles; - else - _autoAssignableRoles.TryRemove(guildId, out _); - - return roles; - } - - public async Task DisableAarAsync(ulong guildId) - { - await using var uow = _db.GetDbContext(); - - await uow.Set().AsNoTracking() - .Where(x => x.GuildId == guildId) - .UpdateAsync(_ => new() - { - AutoAssignRoleIds = null - }); - - _autoAssignableRoles.TryRemove(guildId, out _); - - await uow.SaveChangesAsync(); - } - - public async Task SetAarRolesAsync(ulong guildId, IEnumerable newRoles) - { - await using var uow = _db.GetDbContext(); - - var gc = uow.GuildConfigsForId(guildId, set => set); - gc.SetAutoAssignableRoles(newRoles); - - await uow.SaveChangesAsync(); - } - - public bool TryGetRoles(ulong guildId, out IReadOnlyList roles) - => _autoAssignableRoles.TryGetValue(guildId, out roles); -} - -public static class GuildConfigExtensions -{ - public static List GetAutoAssignableRoles(this GuildConfig gc) - { - if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds)) - return new(); - - return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList(); - } - - public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable roles) - => gc.AutoAssignRoleIds = roles.Join(','); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/AutoPublishService.cs b/src/Ellie.Bot.Modules.Administration/AutoPublishService.cs deleted file mode 100644 index 8c628fd..0000000 --- a/src/Ellie.Bot.Modules.Administration/AutoPublishService.cs +++ /dev/null @@ -1,87 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db.Models; - -namespace Ellie.Modules.Administration.Services; - -public class AutoPublishService : IExecNoCommand, IReadyExecutor, IEService -{ - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly IBotCredsProvider _creds; - private ConcurrentDictionary _enabled; - - public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds) - { - _db = db; - _client = client; - _creds = creds; - } - - public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - { - if (guild is null) - return; - - if (msg.Channel.GetChannelType() != ChannelType.News) - return; - - if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id) - return; - - await msg.CrosspostAsync(new RequestOptions() - { - RetryMode = RetryMode.AlwaysFail - }); - } - - public async Task OnReadyAsync() - { - var creds = _creds.GetCreds(); - - await using var ctx = _db.GetDbContext(); - var items = await ctx.GetTable() - .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId)) - .ToListAsyncLinqToDB(); - - _enabled = items - .ToDictionary(x => x.GuildId, x => x.ChannelId) - .ToConcurrent(); - } - - public async Task ToggleAutoPublish(ulong guildId, ulong channelId) - { - await using var ctx = _db.GetDbContext(); - var deleted = await ctx.GetTable() - .DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId); - - if (deleted != 0) - { - _enabled.TryRemove(guildId, out _); - return false; - } - - await ctx.GetTable() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - ChannelId = channelId, - DateAdded = DateTime.UtcNow, - }, - old => new() - { - ChannelId = channelId, - DateAdded = DateTime.UtcNow, - }, - () => new() - { - GuildId = guildId - }); - - _enabled[guildId] = channelId; - - return true; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommands.cs b/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommands.cs deleted file mode 100644 index 4263be2..0000000 --- a/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommands.cs +++ /dev/null @@ -1,80 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -#if !GLOBAL_ELLIE -namespace Ellie.Modules.Administration -{ - public partial class Administration - { - [Group] - [OwnerOnly] - public partial class DangerousCommands : EllieModule - { - [Cmd] - [OwnerOnly] - public Task SqlSelect([Leftover] string sql) - { - var result = _service.SelectSql(sql); - - return ctx.SendPaginatedConfirmAsync(0, - cur => - { - var items = result.Results.Skip(cur * 20).Take(20).ToList(); - - if (!items.Any()) - return _eb.Create().WithErrorColor().WithFooter(sql).WithDescription("-"); - - return _eb.Create() - .WithOkColor() - .WithFooter(sql) - .WithTitle(string.Join(" ║ ", result.ColumnNames)) - .WithDescription(string.Join('\n', items.Select(x => string.Join(" ║ ", x)))); - }, - result.Results.Count, - 20); - } - - [Cmd] - [OwnerOnly] - public async Task SqlExec([Leftover] string sql) - { - try - { - var embed = _eb.Create() - .WithTitle(GetText(strs.sql_confirm_exec)) - .WithDescription(Format.Code(sql)); - - if (!await PromptUserConfirmAsync(embed)) - return; - - var res = await _service.ExecuteSql(sql); - await SendConfirmAsync(res.ToString()); - } - catch (Exception ex) - { - await SendErrorAsync(ex.ToString()); - } - } - - [Cmd] - [OwnerOnly] - public async Task PurgeUser(ulong userId) - { - var embed = _eb.Create() - .WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString())))); - - if (!await PromptUserConfirmAsync(embed)) - return; - - await _service.PurgeUserAsync(userId); - await ctx.OkAsync(); - } - - [Cmd] - [OwnerOnly] - public Task PurgeUser([Leftover] IUser user) - => PurgeUser(user.Id); - } - } -} -#endif \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommandsService.cs b/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommandsService.cs deleted file mode 100644 index 57b64b3..0000000 --- a/src/Ellie.Bot.Modules.Administration/DangerousCommands/DangerousCommandsService.cs +++ /dev/null @@ -1,104 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Db.Models; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public class DangerousCommandsService : IEService -{ - private readonly DbService _db; - - public DangerousCommandsService(DbService db) - => _db = db; - - public async Task ExecuteSql(string sql) - { - int res; - await using var uow = _db.GetDbContext(); - res = await uow.Database.ExecuteSqlRawAsync(sql); - return res; - } - - public SelectResult SelectSql(string sql) - { - var result = new SelectResult - { - ColumnNames = new(), - Results = new() - }; - - using var uow = _db.GetDbContext(); - var conn = uow.Database.GetDbConnection(); - conn.Open(); - using var cmd = conn.CreateCommand(); - cmd.CommandText = sql; - using var reader = cmd.ExecuteReader(); - if (reader.HasRows) - { - for (var i = 0; i < reader.FieldCount; i++) - result.ColumnNames.Add(reader.GetName(i)); - while (reader.Read()) - { - var obj = new object[reader.FieldCount]; - reader.GetValues(obj); - result.Results.Add(obj.Select(x => x.ToString()).ToArray()); - } - } - - return result; - } - - public async Task PurgeUserAsync(ulong userId) - { - await using var uow = _db.GetDbContext(); - - // get waifu info - var wi = await uow.Set().FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId); - - // if it exists, delete waifu related things - if (wi is not null) - { - // remove updates which have new or old as this waifu - await uow.Set().DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId); - - // delete all items this waifu owns - await uow.Set().DeleteAsync(x => x.WaifuInfoId == wi.Id); - - // all waifus this waifu claims are released - await uow.Set() - .AsQueryable() - .Where(x => x.Claimer.UserId == userId) - .UpdateAsync(x => new() - { - ClaimerId = null - }); - - // all affinities set to this waifu are reset - await uow.Set() - .AsQueryable() - .Where(x => x.Affinity.UserId == userId) - .UpdateAsync(x => new() - { - AffinityId = null - }); - } - - // delete guild xp - await uow.Set().DeleteAsync(x => x.UserId == userId); - - // delete currency transactions - await uow.Set().DeleteAsync(x => x.UserId == userId); - - // delete user, currency, and clubs go away with it - await uow.Set().DeleteAsync(u => u.UserId == userId); - } - - public class SelectResult - { - public List ColumnNames { get; set; } - public List Results { get; set; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelCommands.cs b/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelCommands.cs deleted file mode 100644 index 0171a3f..0000000 --- a/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelCommands.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class GameVoiceChannelCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.MoveMembers)] - public async Task GameVoiceChannel() - { - var vch = ((IGuildUser)ctx.User).VoiceChannel; - - if (vch is null) - { - await ReplyErrorLocalizedAsync(strs.not_in_voice); - return; - } - - var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id); - - if (id is null) - await ReplyConfirmLocalizedAsync(strs.gvc_disabled); - else - { - _service.GameVoiceChannels.Add(vch.Id); - await ReplyConfirmLocalizedAsync(strs.gvc_enabled(Format.Bold(vch.Name))); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelService.cs b/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelService.cs deleted file mode 100644 index 5f4d415..0000000 --- a/src/Ellie.Bot.Modules.Administration/GameVoiceChannel/GameVoiceChannelService.cs +++ /dev/null @@ -1,127 +0,0 @@ -#nullable disable -using Ellie.Db; - -namespace Ellie.Modules.Administration.Services; - -public class GameVoiceChannelService : IEService -{ - public ConcurrentHashSet GameVoiceChannels { get; } - - private readonly DbService _db; - private readonly DiscordSocketClient _client; - - public GameVoiceChannelService(DiscordSocketClient client, DbService db, IBot bot) - { - _db = db; - _client = client; - - GameVoiceChannels = new(bot.AllGuildConfigs - .Where(gc => gc.GameVoiceChannel is not null) - .Select(gc => gc.GameVoiceChannel!.Value)); - - _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; - _client.PresenceUpdated += OnPresenceUpdate; - } - - private Task OnPresenceUpdate(SocketUser socketUser, SocketPresence before, SocketPresence after) - { - _ = Task.Run(async () => - { - try - { - if (socketUser is not SocketGuildUser newUser) - return; - // if the user is in the voice channel and that voice channel is gvc - - if (newUser.VoiceChannel is not { } vc - || !GameVoiceChannels.Contains(vc.Id)) - return; - - //if the activity has changed, and is a playi1ng activity - foreach (var activity in after.Activities) - { - if (activity is { Type: ActivityType.Playing }) - //trigger gvc - { - if (await TriggerGvc(newUser, activity.Name)) - return; - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error running GuildMemberUpdated in gvc"); - } - }); - return Task.CompletedTask; - } - - public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId) - { - ulong? id; - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - - if (gc.GameVoiceChannel == vchId) - { - GameVoiceChannels.TryRemove(vchId); - id = gc.GameVoiceChannel = null; - } - else - { - if (gc.GameVoiceChannel is not null) - GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value); - GameVoiceChannels.Add(vchId); - id = gc.GameVoiceChannel = vchId; - } - - uow.SaveChanges(); - return id; - } - - private Task OnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) - { - _ = Task.Run(async () => - { - try - { - if (usr is not SocketGuildUser gUser) - return; - - if (newState.VoiceChannel is null) - return; - - if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id)) - return; - - foreach (var game in gUser.Activities.Select(x => x.Name)) - { - if (await TriggerGvc(gUser, game)) - return; - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error running VoiceStateUpdate in gvc"); - } - }); - - return Task.CompletedTask; - } - - private async Task TriggerGvc(SocketGuildUser gUser, string game) - { - if (string.IsNullOrWhiteSpace(game)) - return false; - - game = game.TrimTo(50)!.ToLowerInvariant(); - var vch = gUser.Guild.VoiceChannels.FirstOrDefault(x => x.Name.ToLowerInvariant() == game); - - if (vch is null) - return false; - - await Task.Delay(1000); - await gUser.ModifyAsync(gu => gu.Channel = vch); - return true; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetCommands.cs b/src/Ellie.Bot.Modules.Administration/GreetBye/GreetCommands.cs deleted file mode 100644 index 6eec1b2..0000000 --- a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetCommands.cs +++ /dev/null @@ -1,229 +0,0 @@ -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class GreetCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Boost() - { - var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.boost_on); - else - await ReplyPendingLocalizedAsync(strs.boost_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task BoostDel(int timer = 30) - { - if (timer is < 0 or > 600) - return; - - await _service.SetBoostDel(ctx.Guild.Id, timer); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.boostdel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.boostdel_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task BoostMsg([Leftover] string? text = null) - { - if (string.IsNullOrWhiteSpace(text)) - { - var boostMessage = _service.GetBoostMessage(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.boostmsg_cur(boostMessage?.SanitizeMentions())); - return; - } - - var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.boostmsg_new); - if (!sendBoostEnabled) - await ReplyPendingLocalizedAsync(strs.boostmsg_enable($"`{prefix}boost`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDel(int timer = 30) - { - if (timer is < 0 or > 600) - return; - - await _service.SetGreetDel(ctx.Guild.Id, timer); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.greetdel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.greetdel_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Greet() - { - var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.greet_on); - else - await ReplyPendingLocalizedAsync(strs.greet_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetMsg([Leftover] string? text = null) - { - if (string.IsNullOrWhiteSpace(text)) - { - var greetMsg = _service.GetGreetMsg(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.greetmsg_cur(greetMsg?.SanitizeMentions())); - return; - } - - var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.greetmsg_new); - - if (!sendGreetEnabled) - await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDm() - { - var enabled = await _service.SetGreetDm(ctx.Guild.Id); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.greetdm_on); - else - await ReplyConfirmLocalizedAsync(strs.greetdm_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDmMsg([Leftover] string? text = null) - { - if (string.IsNullOrWhiteSpace(text)) - { - var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions())); - return; - } - - var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.greetdmmsg_new); - if (!sendGreetEnabled) - await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Bye() - { - var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.bye_on); - else - await ReplyConfirmLocalizedAsync(strs.bye_off); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task ByeMsg([Leftover] string? text = null) - { - if (string.IsNullOrWhiteSpace(text)) - { - var byeMsg = _service.GetByeMessage(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.byemsg_cur(byeMsg?.SanitizeMentions())); - return; - } - - var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.byemsg_new); - if (!sendByeEnabled) - await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task ByeDel(int timer = 30) - { - await _service.SetByeDel(ctx.Guild.Id, timer); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.byedel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.byedel_off); - } - - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task ByeTest([Leftover] IGuildUser? user = null) - { - user ??= (IGuildUser)ctx.User; - - await _service.ByeTest((ITextChannel)ctx.Channel, user); - var enabled = _service.GetByeEnabled(ctx.Guild.Id); - if (!enabled) - await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task GreetTest([Leftover] IGuildUser? user = null) - { - user ??= (IGuildUser)ctx.User; - - await _service.GreetTest((ITextChannel)ctx.Channel, user); - var enabled = _service.GetGreetEnabled(ctx.Guild.Id); - if (!enabled) - await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task GreetDmTest([Leftover] IGuildUser? user = null) - { - user ??= (IGuildUser)ctx.User; - - var success = await _service.GreetDmTest(user); - if (success) - await ctx.OkAsync(); - else - await ctx.WarningAsync(); - var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id); - if (!enabled) - await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`")); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetGrouper.cs b/src/Ellie.Bot.Modules.Administration/GreetBye/GreetGrouper.cs deleted file mode 100644 index 0ed4d81..0000000 --- a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetGrouper.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace Ellie.Services; - -public class GreetGrouper -{ - private readonly Dictionary> _group; - private readonly object _locker = new(); - - public GreetGrouper() - => _group = new(); - - - /// - /// Creates a group, if group already exists, adds the specified user - /// - /// Id of the server for which to create group for - /// User to add if group already exists - /// - public bool CreateOrAdd(ulong guildId, T toAddIfExists) - { - lock (_locker) - { - if (_group.TryGetValue(guildId, out var list)) - { - list.Add(toAddIfExists); - return false; - } - - _group[guildId] = new(); - return true; - } - } - - /// - /// Remove the specified amount of items from the group. If all items are removed, group will be removed. - /// - /// Id of the group - /// Maximum number of items to retrieve - /// Items retrieved - /// Whether the group has no more items left and is deleted - public bool ClearGroup(ulong guildId, int count, out IReadOnlyCollection items) - { - lock (_locker) - { - if (_group.TryGetValue(guildId, out var set)) - { - // if we want more than there are, return everything - if (count >= set.Count) - { - items = set; - _group.Remove(guildId); - return true; - } - - // if there are more in the group than what's needed - // take the requested number, remove them from the set - // and return them - var toReturn = set.TakeWhile(_ => count-- != 0).ToList(); - foreach (var item in toReturn) - set.Remove(item); - - items = toReturn; - // returning falsemeans group is not yet deleted - // because there are items left - return false; - } - - items = Array.Empty(); - return true; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetService.cs b/src/Ellie.Bot.Modules.Administration/GreetBye/GreetService.cs deleted file mode 100644 index 5e449c2..0000000 --- a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetService.cs +++ /dev/null @@ -1,619 +0,0 @@ -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Services.Database.Models; -using System.Threading.Channels; - -namespace Ellie.Services; - -public class GreetService : IEService, IReadyExecutor -{ - public bool GroupGreets - => _bss.Data.GroupGreets; - - private readonly DbService _db; - - private readonly ConcurrentDictionary _guildConfigsCache; - private readonly DiscordSocketClient _client; - - private readonly GreetGrouper _greets = new(); - private readonly GreetGrouper _byes = new(); - private readonly BotConfigService _bss; - - public GreetService( - DiscordSocketClient client, - IBot bot, - DbService db, - BotConfigService bss) - { - _db = db; - _client = client; - _bss = bss; - - _guildConfigsCache = new(bot.AllGuildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create)); - - _client.UserJoined += OnUserJoined; - _client.UserLeft += OnUserLeft; - - bot.JoinedGuild += OnBotJoinedGuild; - _client.LeftGuild += OnClientLeftGuild; - - _client.GuildMemberUpdated += ClientOnGuildMemberUpdated; - } - - public async Task OnReadyAsync() - { - while (true) - { - var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync(); - var res = await GreetDmUserInternal(conf, user); - compl.TrySetResult(res); - await Task.Delay(2000); - } - } - - private Task ClientOnGuildMemberUpdated(Cacheable optOldUser, SocketGuildUser newUser) - { - // if user is a new booster - // or boosted again the same server - if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null }) - || (optOldUser.Value?.PremiumSince is { } oldDate - && newUser.PremiumSince is { } newDate - && newDate > oldDate)) - { - var conf = GetOrAddSettingsForGuild(newUser.Guild.Id); - if (!conf.SendBoostMessage) - return Task.CompletedTask; - - _ = Task.Run(TriggerBoostMessage(conf, newUser)); - } - - return Task.CompletedTask; - } - - private Func TriggerBoostMessage(GreetSettings conf, SocketGuildUser user) - => async () => - { - var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId); - if (channel is null) - return; - - if (string.IsNullOrWhiteSpace(conf.BoostMessage)) - return; - - var toSend = SmartText.CreateFrom(conf.BoostMessage); - var rep = new ReplacementBuilder().WithDefault(user, channel, user.Guild, _client).Build(); - - try - { - var toDelete = await channel.SendAsync(rep.Replace(toSend)); - if (conf.BoostMessageDeleteAfter > 0) - toDelete.DeleteAfter(conf.BoostMessageDeleteAfter); - } - catch (Exception ex) - { - Log.Error(ex, "Error sending boost message"); - } - }; - - private Task OnClientLeftGuild(SocketGuild arg) - { - _guildConfigsCache.TryRemove(arg.Id, out _); - return Task.CompletedTask; - } - - private Task OnBotJoinedGuild(GuildConfig gc) - { - _guildConfigsCache[gc.GuildId] = GreetSettings.Create(gc); - return Task.CompletedTask; - } - - private Task OnUserLeft(SocketGuild guild, SocketUser user) - { - _ = Task.Run(async () => - { - try - { - var conf = GetOrAddSettingsForGuild(guild.Id); - - if (!conf.SendChannelByeMessage) - return; - var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId); - - if (channel is null) //maybe warn the server owner that the channel is missing - return; - - if (GroupGreets) - { - // if group is newly created, greet that user right away, - // but any user which joins in the next 5 seconds will - // be greeted in a group greet - if (_byes.CreateOrAdd(guild.Id, user)) - { - // greet single user - await ByeUsers(conf, channel, new[] { user }); - var groupClear = false; - while (!groupClear) - { - await Task.Delay(5000); - groupClear = _byes.ClearGroup(guild.Id, 5, out var toBye); - await ByeUsers(conf, channel, toBye); - } - } - } - else - await ByeUsers(conf, channel, new[] { user }); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - public string? GetDmGreetMsg(ulong id) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(id, set => set).DmGreetMessageText; - } - - public string? GetGreetMsg(ulong gid) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(gid, set => set).ChannelGreetMessageText; - } - - public string? GetBoostMessage(ulong gid) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(gid, set => set).BoostMessage; - } - - private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user) - => ByeUsers(conf, channel, new[] { user }); - - private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection users) - { - if (!users.Any()) - return; - - var rep = new ReplacementBuilder().WithChannel(channel) - .WithClient(_client) - .WithServer(_client, (SocketGuild)channel.Guild) - .WithManyUsers(users) - .Build(); - - var text = SmartText.CreateFrom(conf.ChannelByeMessageText); - text = rep.Replace(text); - try - { - var toDelete = await channel.SendAsync(text); - if (conf.AutoDeleteByeMessagesTimer > 0) - toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); - } - catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel) - { - Log.Warning(ex, "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", channel.GuildId); - await SetBye(channel.GuildId, channel.Id, false); - } - catch (Exception ex) - { - Log.Warning(ex, "Error embeding bye message"); - } - } - - private Task GreetUsers(GreetSettings conf, ITextChannel channel, IGuildUser user) - => GreetUsers(conf, channel, new[] { user }); - - private async Task GreetUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection users) - { - if (users.Count == 0) - return; - - var rep = new ReplacementBuilder().WithChannel(channel) - .WithClient(_client) - .WithServer(_client, (SocketGuild)channel.Guild) - .WithManyUsers(users) - .Build(); - - var text = SmartText.CreateFrom(conf.ChannelGreetMessageText); - text = rep.Replace(text); - try - { - var toDelete = await channel.SendAsync(text); - if (conf.AutoDeleteGreetMessagesTimer > 0) - toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); - } - catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel) - { - Log.Warning(ex, "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", channel.GuildId); - await SetGreet(channel.GuildId, channel.Id, false); - } - catch (Exception ex) - { - Log.Warning(ex, "Error embeding greet message"); - } - } - - private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource)> _greetDmQueue = - Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource)>(new BoundedChannelOptions(60) - { - // The limit of 60 users should be only hit when there's a raid. In that case - // probably the best thing to do is to drop newest (raiding) users - FullMode = BoundedChannelFullMode.DropNewest - }); - - private async Task GreetDmUser(GreetSettings conf, IGuildUser user) - { - var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource)); - return await completionSource.Task; - } - - private async Task GreetDmUserInternal(GreetSettings conf, IGuildUser user) - { - try - { - var rep = new ReplacementBuilder() - .WithUser(user) - .WithServer(_client, (SocketGuild)user.Guild) - .Build(); - - var text = SmartText.CreateFrom(conf.DmGreetMessageText); - text = rep.Replace(text); - - if (text is SmartPlainText pt) - { - text = new SmartEmbedText() - { - Description = pt.Text - }; - } - - if (text is SmartEmbedText set) - { - text = set with - { - Footer = CreateFooterSource(user) - }; - } - else if (text is SmartEmbedTextArray seta) - { - // if the greet dm message is a text array - var ebElem = seta.Embeds.LastOrDefault(); - if (ebElem is null) - { - // if there are no embeds, add an embed with the footer - text = seta with - { - Embeds = new[] - { - new SmartEmbedArrayElementText() - { - Footer = CreateFooterSource(user) - } - } - }; - } - else - { - // if the maximum amount of embeds is reached, edit the last embed - if (seta.Embeds.Length >= 10) - { - seta.Embeds[^1] = seta.Embeds[^1] with - { - Footer = CreateFooterSource(user) - }; - } - else - { - // if there is less than 10 embeds, add an embed with footer only - seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText() - { - Footer = CreateFooterSource(user) - }).ToArray(); - } - } - } - - await user.SendAsync(text); - } - catch - { - return false; - } - - return true; - } - - private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user) - => new() - { - Text = $"This message was sent from {user.Guild} server.", - IconUrl = user.Guild.IconUrl - }; - - private Task OnUserJoined(IGuildUser user) - { - _ = Task.Run(async () => - { - try - { - var conf = GetOrAddSettingsForGuild(user.GuildId); - - if (conf.SendChannelGreetMessage) - { - var channel = await user.Guild.GetTextChannelAsync(conf.GreetMessageChannelId); - if (channel is not null) - { - if (GroupGreets) - { - // if group is newly created, greet that user right away, - // but any user which joins in the next 5 seconds will - // be greeted in a group greet - if (_greets.CreateOrAdd(user.GuildId, user)) - { - // greet single user - await GreetUsers(conf, channel, new[] { user }); - var groupClear = false; - while (!groupClear) - { - await Task.Delay(5000); - groupClear = _greets.ClearGroup(user.GuildId, 5, out var toGreet); - await GreetUsers(conf, channel, toGreet); - } - } - } - else - await GreetUsers(conf, channel, new[] { user }); - } - } - - if (conf.SendDmGreetMessage) - await GreetDmUser(conf, user); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - public string? GetByeMessage(ulong gid) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(gid, set => set).ChannelByeMessageText; - } - - public GreetSettings GetOrAddSettingsForGuild(ulong guildId) - { - if (_guildConfigsCache.TryGetValue(guildId, out var settings)) - return settings; - - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set); - settings = GreetSettings.Create(gc); - } - - _guildConfigsCache.TryAdd(guildId, settings); - return settings; - } - - public async Task SetGreet(ulong guildId, ulong channelId, bool? value = null) - { - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - var enabled = conf.SendChannelGreetMessage = value ?? !conf.SendChannelGreetMessage; - conf.GreetMessageChannelId = channelId; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - return enabled; - } - - public bool SetGreetMessage(ulong guildId, ref string message) - { - message = message.SanitizeMentions(); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentNullException(nameof(message)); - - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.ChannelGreetMessageText = message; - var greetMsgEnabled = conf.SendChannelGreetMessage; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache.AddOrUpdate(guildId, toAdd, (_, _) => toAdd); - - uow.SaveChanges(); - return greetMsgEnabled; - } - - public async Task SetGreetDm(ulong guildId, bool? value = null) - { - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - var enabled = conf.SendDmGreetMessage = value ?? !conf.SendDmGreetMessage; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - return enabled; - } - - public bool SetGreetDmMessage(ulong guildId, ref string? message) - { - message = message?.SanitizeMentions(); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentNullException(nameof(message)); - - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.DmGreetMessageText = message; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - uow.SaveChanges(); - return conf.SendDmGreetMessage; - } - - public async Task SetBye(ulong guildId, ulong channelId, bool? value = null) - { - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - var enabled = conf.SendChannelByeMessage = value ?? !conf.SendChannelByeMessage; - conf.ByeMessageChannelId = channelId; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - return enabled; - } - - public bool SetByeMessage(ulong guildId, ref string? message) - { - message = message?.SanitizeMentions(); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentNullException(nameof(message)); - - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.ChannelByeMessageText = message; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - uow.SaveChanges(); - return conf.SendChannelByeMessage; - } - - public async Task SetByeDel(ulong guildId, int timer) - { - if (timer is < 0 or > 600) - return; - - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.AutoDeleteByeMessagesTimer = timer; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - } - - public async Task SetGreetDel(ulong guildId, int timer) - { - if (timer is < 0 or > 600) - return; - - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.AutoDeleteGreetMessagesTimer = timer; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - } - - public bool SetBoostMessage(ulong guildId, ref string message) - { - message = message.SanitizeMentions(); - - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.BoostMessage = message; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - uow.SaveChanges(); - return conf.SendBoostMessage; - } - - public async Task SetBoostDel(ulong guildId, int timer) - { - if (timer is < 0 or > 600) - throw new ArgumentOutOfRangeException(nameof(timer)); - - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.BoostMessageDeleteAfter = timer; - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - - await uow.SaveChangesAsync(); - } - - public async Task ToggleBoost(ulong guildId, ulong channelId) - { - await using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - conf.SendBoostMessage = !conf.SendBoostMessage; - conf.BoostMessageChannelId = channelId; - await uow.SaveChangesAsync(); - - var toAdd = GreetSettings.Create(conf); - _guildConfigsCache[guildId] = toAdd; - return conf.SendBoostMessage; - } - - #region Get Enabled Status - - public bool GetGreetDmEnabled(ulong guildId) - { - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - return conf.SendDmGreetMessage; - } - - public bool GetGreetEnabled(ulong guildId) - { - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - return conf.SendChannelGreetMessage; - } - - public bool GetByeEnabled(ulong guildId) - { - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set); - return conf.SendChannelByeMessage; - } - - #endregion - - #region Test Messages - - public Task ByeTest(ITextChannel channel, IGuildUser user) - { - var conf = GetOrAddSettingsForGuild(user.GuildId); - return ByeUsers(conf, channel, user); - } - - public Task GreetTest(ITextChannel channel, IGuildUser user) - { - var conf = GetOrAddSettingsForGuild(user.GuildId); - return GreetUsers(conf, channel, user); - } - - public Task GreetDmTest(IGuildUser user) - { - var conf = GetOrAddSettingsForGuild(user.GuildId); - return GreetDmUser(conf, user); - } - - #endregion -} diff --git a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetSettings.cs b/src/Ellie.Bot.Modules.Administration/GreetBye/GreetSettings.cs deleted file mode 100644 index 6361841..0000000 --- a/src/Ellie.Bot.Modules.Administration/GreetBye/GreetSettings.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Services; - -public class GreetSettings -{ - public int AutoDeleteGreetMessagesTimer { get; set; } - public int AutoDeleteByeMessagesTimer { get; set; } - - public ulong GreetMessageChannelId { get; set; } - public ulong ByeMessageChannelId { get; set; } - - public bool SendDmGreetMessage { get; set; } - public string? DmGreetMessageText { get; set; } - - public bool SendChannelGreetMessage { get; set; } - public string? ChannelGreetMessageText { get; set; } - - public bool SendChannelByeMessage { get; set; } - public string? ChannelByeMessageText { get; set; } - - public bool SendBoostMessage { get; set; } - public string? BoostMessage { get; set; } - public int BoostMessageDeleteAfter { get; set; } - public ulong BoostMessageChannelId { get; set; } - - public static GreetSettings Create(GuildConfig g) - => new() - { - AutoDeleteByeMessagesTimer = g.AutoDeleteByeMessagesTimer, - AutoDeleteGreetMessagesTimer = g.AutoDeleteGreetMessagesTimer, - GreetMessageChannelId = g.GreetMessageChannelId, - ByeMessageChannelId = g.ByeMessageChannelId, - SendDmGreetMessage = g.SendDmGreetMessage, - DmGreetMessageText = g.DmGreetMessageText, - SendChannelGreetMessage = g.SendChannelGreetMessage, - ChannelGreetMessageText = g.ChannelGreetMessageText, - SendChannelByeMessage = g.SendChannelByeMessage, - ChannelByeMessageText = g.ChannelByeMessageText, - SendBoostMessage = g.SendBoostMessage, - BoostMessage = g.BoostMessage, - BoostMessageDeleteAfter = g.BoostMessageDeleteAfter, - BoostMessageChannelId = g.BoostMessageChannelId - }; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/ImageOnlyChannelService.cs b/src/Ellie.Bot.Modules.Administration/ImageOnlyChannelService.cs deleted file mode 100644 index 3f4775e..0000000 --- a/src/Ellie.Bot.Modules.Administration/ImageOnlyChannelService.cs +++ /dev/null @@ -1,235 +0,0 @@ -#nullable disable -using LinqToDB; -using Microsoft.Extensions.Caching.Memory; -using Ellie.Common.ModuleBehaviors; -using System.Net; -using System.Threading.Channels; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public sealed class SomethingOnlyChannelService : IExecOnMessage -{ - public int Priority { get; } = 0; - private readonly IMemoryCache _ticketCache; - private readonly DiscordSocketClient _client; - private readonly DbService _db; - private readonly ConcurrentDictionary> _imageOnly; - private readonly ConcurrentDictionary> _linkOnly; - - private readonly Channel _deleteQueue = Channel.CreateBounded( - new BoundedChannelOptions(100) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = true, - SingleWriter = false - }); - - - public SomethingOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db) - { - _ticketCache = ticketCache; - _client = client; - _db = db; - - using var uow = _db.GetDbContext(); - _imageOnly = uow.Set() - .Where(x => x.Type == OnlyChannelType.Image) - .ToList() - .GroupBy(x => x.GuildId) - .ToDictionary(x => x.Key, x => new ConcurrentHashSet(x.Select(y => y.ChannelId))) - .ToConcurrent(); - - _linkOnly = uow.Set() - .Where(x => x.Type == OnlyChannelType.Link) - .ToList() - .GroupBy(x => x.GuildId) - .ToDictionary(x => x.Key, x => new ConcurrentHashSet(x.Select(y => y.ChannelId))) - .ToConcurrent(); - - _ = Task.Run(DeleteQueueRunner); - - _client.ChannelDestroyed += ClientOnChannelDestroyed; - } - - private async Task ClientOnChannelDestroyed(SocketChannel ch) - { - if (ch is not IGuildChannel gch) - return; - - if (_imageOnly.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id)) - await ToggleImageOnlyChannelAsync(gch.GuildId, ch.Id, true); - } - - private async Task DeleteQueueRunner() - { - while (true) - { - var toDelete = await _deleteQueue.Reader.ReadAsync(); - try - { - await toDelete.DeleteAsync(); - await Task.Delay(1000); - } - catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) - { - // disable if bot can't delete messages in the channel - await ToggleImageOnlyChannelAsync(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true); - } - } - } - - public async Task ToggleImageOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false) - { - var newState = false; - await using var uow = _db.GetDbContext(); - if (forceDisable || (_imageOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId))) - { - await uow.Set().DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Image); - } - else - { - await uow.Set().DeleteAsync(x => x.ChannelId == channelId); - uow.Set().Add(new() - { - GuildId = guildId, - ChannelId = channelId, - Type = OnlyChannelType.Image - }); - - if (_linkOnly.TryGetValue(guildId, out var chs)) - chs.TryRemove(channelId); - - channels = _imageOnly.GetOrAdd(guildId, new ConcurrentHashSet()); - channels.Add(channelId); - newState = true; - } - - await uow.SaveChangesAsync(); - return newState; - } - - public async Task ToggleLinkOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false) - { - var newState = false; - await using var uow = _db.GetDbContext(); - if (forceDisable || (_linkOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId))) - { - await uow.Set().DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Link); - } - else - { - await uow.Set().DeleteAsync(x => x.ChannelId == channelId); - uow.Set().Add(new() - { - GuildId = guildId, - ChannelId = channelId, - Type = OnlyChannelType.Link - }); - - if (_imageOnly.TryGetValue(guildId, out var chs)) - chs.TryRemove(channelId); - - channels = _linkOnly.GetOrAdd(guildId, new ConcurrentHashSet()); - channels.Add(channelId); - newState = true; - } - - await uow.SaveChangesAsync(); - return newState; - } - - public async Task ExecOnMessageAsync(IGuild guild, IUserMessage msg) - { - if (msg.Channel is not ITextChannel tch) - return false; - - if (_imageOnly.TryGetValue(tch.GuildId, out var chs) && chs.Contains(msg.Channel.Id)) - return await HandleOnlyChannel(tch, msg, OnlyChannelType.Image); - - if (_linkOnly.TryGetValue(tch.GuildId, out chs) && chs.Contains(msg.Channel.Id)) - return await HandleOnlyChannel(tch, msg, OnlyChannelType.Link); - - return false; - } - - private async Task HandleOnlyChannel(ITextChannel tch, IUserMessage msg, OnlyChannelType type) - { - if (type == OnlyChannelType.Image) - { - if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 })) - return false; - } - else - { - if (msg.Content.TryGetUrlPath(out _)) - return false; - } - - var user = await tch.Guild.GetUserAsync(msg.Author.Id) - ?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id); - - if (user is null) - return false; - - // ignore owner and admin - if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator) - { - Log.Information("{Type}-Only Channel: Ignoring owner od admin ({ChannelId})", type, msg.Channel.Id); - return false; - } - - // ignore users higher in hierarchy - var botUser = await tch.Guild.GetCurrentUserAsync(); - if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position)) - return false; - - if (!botUser.GetPermissions(tch).ManageChannel) - { - if(type == OnlyChannelType.Image) - await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true); - else - await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true); - - return false; - } - - var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id); - if (shouldLock) - { - await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny)); - Log.Warning("{Type}-Only Channel: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]", - type, - msg.Author, - msg.Author.Id, - msg.Channel.Id); - } - - try - { - await _deleteQueue.Writer.WriteAsync(msg); - } - catch (Exception ex) - { - Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}", msg.Id, tch.Id); - } - - return true; - } - - private bool AddUserTicket(ulong guildId, ulong userId) - { - var old = _ticketCache.GetOrCreate($"{guildId}_{userId}", - entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); - return 0; - }); - - _ticketCache.Set($"{guildId}_{userId}", ++old); - - // if this is the third time that the user posts a - // non image in an image-only channel on this server - return old > 2; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/LocalizationCommands.cs b/src/Ellie.Bot.Modules.Administration/LocalizationCommands.cs deleted file mode 100644 index 9acb3bb..0000000 --- a/src/Ellie.Bot.Modules.Administration/LocalizationCommands.cs +++ /dev/null @@ -1,250 +0,0 @@ -#nullable disable -using System.Globalization; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class LocalizationCommands : EllieModule - { - private static readonly IReadOnlyDictionary _supportedLocales = new Dictionary - { - { "ar", "العربية" }, - { "zh-TW", "繁體中文, 台灣" }, - { "zh-CN", "简体中文, 中华人民共和国" }, - { "nl-NL", "Nederlands, Nederland" }, - { "en-US", "English, United States" }, - { "fr-FR", "Français, France" }, - { "cs-CZ", "Čeština, Česká republika" }, - { "da-DK", "Dansk, Danmark" }, - { "de-DE", "Deutsch, Deutschland" }, - { "he-IL", "עברית, ישראל" }, - { "hu-HU", "Magyar, Magyarország" }, - { "id-ID", "Bahasa Indonesia, Indonesia" }, - { "it-IT", "Italiano, Italia" }, - { "ja-JP", "日本語, 日本" }, - { "ko-KR", "한국어, 대한민국" }, - { "nb-NO", "Norsk, Norge" }, - { "pl-PL", "Polski, Polska" }, - { "pt-BR", "Português Brasileiro, Brasil" }, - { "ro-RO", "Română, România" }, - { "ru-RU", "Русский, Россия" }, - { "sr-Cyrl-RS", "Српски, Србија" }, - { "es-ES", "Español, España" }, - { "sv-SE", "Svenska, Sverige" }, - { "tr-TR", "Türkçe, Türkiye" }, - { "ts-TS", "Tsundere, You Baka" }, - { "uk-UA", "Українська, Україна" } - }; - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task LanguageSet() - => await ReplyConfirmLocalizedAsync(strs.lang_set_show(Format.Bold(Culture.ToString()), - Format.Bold(Culture.NativeName))); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public async Task LanguageSet(string name) - { - try - { - CultureInfo ci; - if (name.Trim().ToLowerInvariant() == "default") - { - _localization.RemoveGuildCulture(ctx.Guild); - ci = _localization.DefaultCultureInfo; - } - else - { - ci = new(name); - _localization.SetGuildCulture(ctx.Guild, ci); - } - - await ReplyConfirmLocalizedAsync(strs.lang_set(Format.Bold(ci.ToString()), Format.Bold(ci.NativeName))); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.lang_set_fail); - } - } - - [Cmd] - public async Task LanguageSetDefault() - { - var cul = _localization.DefaultCultureInfo; - await ReplyErrorLocalizedAsync(strs.lang_set_bot_show(cul, cul.NativeName)); - } - - [Cmd] - [OwnerOnly] - public async Task LanguageSetDefault(string name) - { - try - { - CultureInfo ci; - if (name.Trim().ToLowerInvariant() == "default") - { - _localization.ResetDefaultCulture(); - ci = _localization.DefaultCultureInfo; - } - else - { - ci = new(name); - _localization.SetDefaultCulture(ci); - } - - await ReplyConfirmLocalizedAsync(strs.lang_set_bot(Format.Bold(ci.ToString()), - Format.Bold(ci.NativeName))); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.lang_set_fail); - } - } - - [Cmd] - public async Task LanguagesList() - => await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.lang_list)) - .WithDescription(string.Join("\n", - _supportedLocales.Select( - x => $"{Format.Code(x.Key),-10} => {x.Value}")))); - } -} -/* list of language codes for reference. - * taken from https://github.com/dotnet/coreclr/blob/ee5862c6a257e60e263537d975ab6c513179d47f/src/mscorlib/src/System/Globalization/CultureData.cs#L192 - { "029", "en-029" }, - { "AE", "ar-AE" }, - { "AF", "prs-AF" }, - { "AL", "sq-AL" }, - { "AM", "hy-AM" }, - { "AR", "es-AR" }, - { "AT", "de-AT" }, - { "AU", "en-AU" }, - { "AZ", "az-Cyrl-AZ" }, - { "BA", "bs-Latn-BA" }, - { "BD", "bn-BD" }, - { "BE", "nl-BE" }, - { "BG", "bg-BG" }, - { "BH", "ar-BH" }, - { "BN", "ms-BN" }, - { "BO", "es-BO" }, - { "BR", "pt-BR" }, - { "BY", "be-BY" }, - { "BZ", "en-BZ" }, - { "CA", "en-CA" }, - { "CH", "it-CH" }, - { "CL", "es-CL" }, - { "CN", "zh-CN" }, - { "CO", "es-CO" }, - { "CR", "es-CR" }, - { "CS", "sr-Cyrl-CS" }, - { "CZ", "cs-CZ" }, - { "DE", "de-DE" }, - { "DK", "da-DK" }, - { "DO", "es-DO" }, - { "DZ", "ar-DZ" }, - { "EC", "es-EC" }, - { "EE", "et-EE" }, - { "EG", "ar-EG" }, - { "ES", "es-ES" }, - { "ET", "am-ET" }, - { "FI", "fi-FI" }, - { "FO", "fo-FO" }, - { "FR", "fr-FR" }, - { "GB", "en-GB" }, - { "GE", "ka-GE" }, - { "GL", "kl-GL" }, - { "GR", "el-GR" }, - { "GT", "es-GT" }, - { "HK", "zh-HK" }, - { "HN", "es-HN" }, - { "HR", "hr-HR" }, - { "HU", "hu-HU" }, - { "ID", "id-ID" }, - { "IE", "en-IE" }, - { "IL", "he-IL" }, - { "IN", "hi-IN" }, - { "IQ", "ar-IQ" }, - { "IR", "fa-IR" }, - { "IS", "is-IS" }, - { "IT", "it-IT" }, - { "IV", "" }, - { "JM", "en-JM" }, - { "JO", "ar-JO" }, - { "JP", "ja-JP" }, - { "KE", "sw-KE" }, - { "KG", "ky-KG" }, - { "KH", "km-KH" }, - { "KR", "ko-KR" }, - { "KW", "ar-KW" }, - { "KZ", "kk-KZ" }, - { "LA", "lo-LA" }, - { "LB", "ar-LB" }, - { "LI", "de-LI" }, - { "LK", "si-LK" }, - { "LT", "lt-LT" }, - { "LU", "lb-LU" }, - { "LV", "lv-LV" }, - { "LY", "ar-LY" }, - { "MA", "ar-MA" }, - { "MC", "fr-MC" }, - { "ME", "sr-Latn-ME" }, - { "MK", "mk-MK" }, - { "MN", "mn-MN" }, - { "MO", "zh-MO" }, - { "MT", "mt-MT" }, - { "MV", "dv-MV" }, - { "MX", "es-MX" }, - { "MY", "ms-MY" }, - { "NG", "ig-NG" }, - { "NI", "es-NI" }, - { "NL", "nl-NL" }, - { "NO", "nn-NO" }, - { "NP", "ne-NP" }, - { "NZ", "en-NZ" }, - { "OM", "ar-OM" }, - { "PA", "es-PA" }, - { "PE", "es-PE" }, - { "PH", "en-PH" }, - { "PK", "ur-PK" }, - { "PL", "pl-PL" }, - { "PR", "es-PR" }, - { "PT", "pt-PT" }, - { "PY", "es-PY" }, - { "QA", "ar-QA" }, - { "RO", "ro-RO" }, - { "RS", "sr-Latn-RS" }, - { "RU", "ru-RU" }, - { "RW", "rw-RW" }, - { "SA", "ar-SA" }, - { "SE", "sv-SE" }, - { "SG", "zh-SG" }, - { "SI", "sl-SI" }, - { "SK", "sk-SK" }, - { "SN", "wo-SN" }, - { "SV", "es-SV" }, - { "SY", "ar-SY" }, - { "TH", "th-TH" }, - { "TJ", "tg-Cyrl-TJ" }, - { "TM", "tk-TM" }, - { "TN", "ar-TN" }, - { "TR", "tr-TR" }, - { "TT", "en-TT" }, - { "TW", "zh-TW" }, - { "UA", "uk-UA" }, - { "US", "en-US" }, - { "UY", "es-UY" }, - { "UZ", "uz-Cyrl-UZ" }, - { "VE", "es-VE" }, - { "VN", "vi-VN" }, - { "YE", "ar-YE" }, - { "ZA", "af-ZA" }, - { "ZW", "en-ZW" } - */ \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Mute/MuteCommands.cs b/src/Ellie.Bot.Modules.Administration/Mute/MuteCommands.cs deleted file mode 100644 index 17c4d38..0000000 --- a/src/Ellie.Bot.Modules.Administration/Mute/MuteCommands.cs +++ /dev/null @@ -1,231 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders.Models; -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class MuteCommands : EllieModule - { - private async Task VerifyMutePermissions(IGuildUser runnerUser, IGuildUser targetUser) - { - var runnerUserRoles = runnerUser.GetRoles(); - var targetUserRoles = targetUser.GetRoles(); - if (runnerUser.Id != ctx.Guild.OwnerId - && runnerUserRoles.Max(x => x.Position) <= targetUserRoles.Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.mute_perms); - return false; - } - - return true; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task MuteRole([Leftover] IRole role = null) - { - if (role is null) - { - var muteRole = await _service.GetMuteRole(ctx.Guild); - await ReplyConfirmLocalizedAsync(strs.mute_role(Format.Code(muteRole.Name))); - return; - } - - if (ctx.User.Id != ctx.Guild.OwnerId - && role.Position >= ((SocketGuildUser)ctx.User).Roles.Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u); - return; - } - - await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name); - - await ReplyConfirmLocalizedAsync(strs.mute_role_set); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - [Priority(0)] - public async Task Mute(IGuildUser target, [Leftover] string reason = "") - { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, target)) - return; - - await _service.MuteUser(target, ctx.User, reason: reason); - await ReplyConfirmLocalizedAsync(strs.user_muted(Format.Bold(target.ToString()))); - } - catch (Exception ex) - { - Log.Warning(ex, "Exception in the mute command"); - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - [Priority(1)] - public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") - { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) - return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.TimedMute(user, ctx.User, time.Time, reason: reason); - await ReplyConfirmLocalizedAsync(strs.user_muted_time(Format.Bold(user.ToString()), - (int)time.Time.TotalMinutes)); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in mute command"); - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - public async Task Unmute(IGuildUser user, [Leftover] string reason = "") - { - try - { - await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, reason: reason); - await ReplyConfirmLocalizedAsync(strs.user_unmuted(Format.Bold(user.ToString()))); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task ChatMute(IGuildUser user, [Leftover] string reason = "") - { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.MuteUser(user, ctx.User, MuteType.Chat, reason); - await ReplyConfirmLocalizedAsync(strs.user_chat_mute(Format.Bold(user.ToString()))); - } - catch (Exception ex) - { - Log.Warning(ex, "Exception in the chatmute command"); - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") - { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) - return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason); - await ReplyConfirmLocalizedAsync(strs.user_chat_mute_time(Format.Bold(user.ToString()), - (int)time.Time.TotalMinutes)); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in chatmute command"); - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task ChatUnmute(IGuildUser user, [Leftover] string reason = "") - { - try - { - await _service.UnmuteUser(user.Guild.Id, user.Id, ctx.User, MuteType.Chat, reason); - await ReplyConfirmLocalizedAsync(strs.user_chat_unmute(Format.Bold(user.ToString()))); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - [Priority(0)] - public async Task VoiceMute(IGuildUser user, [Leftover] string reason = "") - { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.MuteUser(user, ctx.User, MuteType.Voice, reason); - await ReplyConfirmLocalizedAsync(strs.user_voice_mute(Format.Bold(user.ToString()))); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - [Priority(1)] - public async Task VoiceMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") - { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) - return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason); - await ReplyConfirmLocalizedAsync(strs.user_voice_mute_time(Format.Bold(user.ToString()), - (int)time.Time.TotalMinutes)); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - public async Task VoiceUnmute(IGuildUser user, [Leftover] string reason = "") - { - try - { - await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, MuteType.Voice, reason); - await ReplyConfirmLocalizedAsync(strs.user_voice_unmute(Format.Bold(user.ToString()))); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Mute/MuteService.cs b/src/Ellie.Bot.Modules.Administration/Mute/MuteService.cs deleted file mode 100644 index 50bf52e..0000000 --- a/src/Ellie.Bot.Modules.Administration/Mute/MuteService.cs +++ /dev/null @@ -1,505 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public enum MuteType -{ - Voice, - Chat, - All -} - -public class MuteService : IEService -{ - public enum TimerType { Mute, Ban, AddRole } - - private static readonly OverwritePermissions _denyOverwrite = new(addReactions: PermValue.Deny, - sendMessages: PermValue.Deny, - sendMessagesInThreads: PermValue.Deny, - attachFiles: PermValue.Deny); - - public event Action UserMuted = delegate { }; - public event Action UserUnmuted = delegate { }; - - public ConcurrentDictionary GuildMuteRoles { get; } - public ConcurrentDictionary> MutedUsers { get; } - - public ConcurrentDictionary> UnTimers { get; } = new(); - - private readonly DiscordSocketClient _client; - private readonly DbService _db; - private readonly IEmbedBuilderService _eb; - - public MuteService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb) - { - _client = client; - _db = db; - _eb = eb; - - using (var uow = db.GetDbContext()) - { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set() - .AsNoTracking() - .AsSplitQuery() - .Include(x => x.MutedUsers) - .Include(x => x.UnbanTimer) - .Include(x => x.UnmuteTimers) - .Include(x => x.UnroleTimer) - .Where(x => guildIds.Contains(x.GuildId)) - .ToList(); - - GuildMuteRoles = configs.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) - .ToDictionary(c => c.GuildId, c => c.MuteRoleName) - .ToConcurrent(); - - MutedUsers = new(configs.ToDictionary(k => k.GuildId, - v => new ConcurrentHashSet(v.MutedUsers.Select(m => m.UserId)))); - - var max = TimeSpan.FromDays(49); - - foreach (var conf in configs) - { - foreach (var x in conf.UnmuteTimers) - { - TimeSpan after; - if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - after = TimeSpan.FromMinutes(2); - else - { - var unmute = x.UnmuteAt - DateTime.UtcNow; - after = unmute > max ? max : unmute; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute); - } - - foreach (var x in conf.UnbanTimer) - { - TimeSpan after; - if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - after = TimeSpan.FromMinutes(2); - else - { - var unban = x.UnbanAt - DateTime.UtcNow; - after = unban > max ? max : unban; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban); - } - - foreach (var x in conf.UnroleTimer) - { - TimeSpan after; - if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - after = TimeSpan.FromMinutes(2); - else - { - var unban = x.UnbanAt - DateTime.UtcNow; - after = unban > max ? max : unban; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId); - } - } - - _client.UserJoined += Client_UserJoined; - } - - UserMuted += OnUserMuted; - UserUnmuted += OnUserUnmuted; - } - - private void OnUserMuted( - IGuildUser user, - IUser mod, - MuteType type, - string reason) - { - if (string.IsNullOrWhiteSpace(reason)) - return; - - _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() - .WithDescription( - $"You've been muted in {user.Guild} server") - .AddField("Mute Type", type.ToString()) - .AddField("Moderator", mod.ToString()) - .AddField("Reason", reason) - .Build())); - } - - private void OnUserUnmuted( - IGuildUser user, - IUser mod, - MuteType type, - string reason) - { - if (string.IsNullOrWhiteSpace(reason)) - return; - - _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() - .WithDescription( - $"You've been unmuted in {user.Guild} server") - .AddField("Unmute Type", type.ToString()) - .AddField("Moderator", mod.ToString()) - .AddField("Reason", reason) - .Build())); - } - - private Task Client_UserJoined(IGuildUser usr) - { - try - { - MutedUsers.TryGetValue(usr.Guild.Id, out var muted); - - if (muted is null || !muted.Contains(usr.Id)) - return Task.CompletedTask; - _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute")); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in MuteService UserJoined event"); - } - - return Task.CompletedTask; - } - - public async Task SetMuteRoleAsync(ulong guildId, string name) - { - await using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, set => set); - config.MuteRoleName = name; - GuildMuteRoles.AddOrUpdate(guildId, name, (_, _) => name); - await uow.SaveChangesAsync(); - } - - public async Task MuteUser( - IGuildUser usr, - IUser mod, - MuteType type = MuteType.All, - string reason = "") - { - if (type == MuteType.All) - { - try { await usr.ModifyAsync(x => x.Mute = true); } - catch { } - - var muteRole = await GetMuteRole(usr.Guild); - if (!usr.RoleIds.Contains(muteRole.Id)) - await usr.AddRoleAsync(muteRole); - StopTimer(usr.GuildId, usr.Id, TimerType.Mute); - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(usr.Guild.Id, - set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers)); - config.MutedUsers.Add(new() - { - UserId = usr.Id - }); - if (MutedUsers.TryGetValue(usr.Guild.Id, out var muted)) - muted.Add(usr.Id); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); - - await uow.SaveChangesAsync(); - } - - UserMuted(usr, mod, MuteType.All, reason); - } - else if (type == MuteType.Voice) - { - try - { - await usr.ModifyAsync(x => x.Mute = true); - UserMuted(usr, mod, MuteType.Voice, reason); - } - catch { } - } - else if (type == MuteType.Chat) - { - await usr.AddRoleAsync(await GetMuteRole(usr.Guild)); - UserMuted(usr, mod, MuteType.Chat, reason); - } - } - - public async Task UnmuteUser( - ulong guildId, - ulong usrId, - IUser mod, - MuteType type = MuteType.All, - string reason = "") - { - var usr = _client.GetGuild(guildId)?.GetUser(usrId); - if (type == MuteType.All) - { - StopTimer(guildId, usrId, TimerType.Mute); - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, - set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers)); - var match = new MutedUserId - { - UserId = usrId - }; - var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match)); - if (toRemove is not null) - uow.Remove(toRemove); - if (MutedUsers.TryGetValue(guildId, out var muted)) - muted.TryRemove(usrId); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId); - - await uow.SaveChangesAsync(); - } - - if (usr is not null) - { - try { await usr.ModifyAsync(x => x.Mute = false); } - catch { } - - try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); } - catch - { - /*ignore*/ - } - - UserUnmuted(usr, mod, MuteType.All, reason); - } - } - else if (type == MuteType.Voice) - { - if (usr is null) - return; - try - { - await usr.ModifyAsync(x => x.Mute = false); - UserUnmuted(usr, mod, MuteType.Voice, reason); - } - catch { } - } - else if (type == MuteType.Chat) - { - if (usr is null) - return; - await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); - UserUnmuted(usr, mod, MuteType.Chat, reason); - } - } - - public async Task GetMuteRole(IGuild guild) - { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); - - const string defaultMuteRoleName = "nadeko-mute"; - - var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); - - var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); - if (muteRole is null) - //if it doesn't exist, create it - { - try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false); } - catch - { - //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one - muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) - ?? await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false); - } - } - - foreach (var toOverwrite in await guild.GetTextChannelsAsync()) - { - try - { - if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id - && x.TargetType == PermissionTarget.Role)) - { - await toOverwrite.AddPermissionOverwriteAsync(muteRole, _denyOverwrite); - - await Task.Delay(200); - } - } - catch - { - // ignored - } - } - - return muteRole; - } - - public async Task TimedMute( - IGuildUser user, - IUser mod, - TimeSpan after, - MuteType muteType = MuteType.All, - string reason = "") - { - await MuteUser(user, mod, muteType, reason); // mute the user. This will also remove any previous unmute timers - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers)); - config.UnmuteTimers.Add(new() - { - UserId = user.Id, - UnmuteAt = DateTime.UtcNow + after - }); // add teh unmute timer to the database - uow.SaveChanges(); - } - - StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer - } - - public async Task TimedBan( - IGuild guild, - ulong userId, - TimeSpan after, - string reason, - int pruneDays) - { - await guild.AddBanAsync(userId, pruneDays, reason); - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer)); - config.UnbanTimer.Add(new() - { - UserId = userId, - UnbanAt = DateTime.UtcNow + after - }); // add teh unmute timer to the database - await uow.SaveChangesAsync(); - } - - StartUn_Timer(guild.Id, userId, after, TimerType.Ban); // start the timer - } - - public async Task TimedRole( - IGuildUser user, - TimeSpan after, - string reason, - IRole role) - { - await user.AddRoleAsync(role); - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer)); - config.UnroleTimer.Add(new() - { - UserId = user.Id, - UnbanAt = DateTime.UtcNow + after, - RoleId = role.Id - }); // add teh unmute timer to the database - uow.SaveChanges(); - } - - StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer - } - - public void StartUn_Timer( - ulong guildId, - ulong userId, - TimeSpan after, - TimerType type, - ulong? roleId = null) - { - //load the unmute timers for this guild - var userUnTimers = UnTimers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>()); - - //unmute timer to be added - var toAdd = new Timer(async _ => - { - if (type == TimerType.Ban) - { - try - { - RemoveTimerFromDb(guildId, userId, type); - StopTimer(guildId, userId, type); - var guild = _client.GetGuild(guildId); // load the guild - if (guild is not null) - await guild.RemoveBanAsync(userId); - } - catch (Exception ex) - { - Log.Warning(ex, "Couldn't unban user {UserId} in guild {GuildId}", userId, guildId); - } - } - else if (type == TimerType.AddRole) - { - try - { - if (roleId is null) - return; - - RemoveTimerFromDb(guildId, userId, type); - StopTimer(guildId, userId, type); - var guild = _client.GetGuild(guildId); - var user = guild?.GetUser(userId); - var role = guild?.GetRole(roleId.Value); - if (guild is not null && user is not null && user.Roles.Contains(role)) - await user.RemoveRoleAsync(role); - } - catch (Exception ex) - { - Log.Warning(ex, "Couldn't remove role from user {UserId} in guild {GuildId}", userId, guildId); - } - } - else - { - try - { - // unmute the user, this will also remove the timer from the db - await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired"); - } - catch (Exception ex) - { - RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db - Log.Warning(ex, "Couldn't unmute user {UserId} in guild {GuildId}", userId, guildId); - } - } - }, - null, - after, - Timeout.InfiniteTimeSpan); - - //add it, or stop the old one and add this one - userUnTimers.AddOrUpdate((userId, type), - _ => toAdd, - (_, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return toAdd; - }); - } - - public void StopTimer(ulong guildId, ulong userId, TimerType type) - { - if (!UnTimers.TryGetValue(guildId, out var userTimer)) - return; - - if (userTimer.TryRemove((userId, type), out var removed)) - removed.Change(Timeout.Infinite, Timeout.Infinite); - } - - private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type) - { - using var uow = _db.GetDbContext(); - object toDelete; - if (type == TimerType.Mute) - { - var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnmuteTimers)); - toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId); - } - else - { - var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnbanTimer)); - toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId); - } - - if (toDelete is not null) - uow.Remove(toDelete); - uow.SaveChanges(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/PermOverrides/DiscordPermOverrideCommands.cs b/src/Ellie.Bot.Modules.Administration/PermOverrides/DiscordPermOverrideCommands.cs deleted file mode 100644 index 896ce3b..0000000 --- a/src/Ellie.Bot.Modules.Administration/PermOverrides/DiscordPermOverrideCommands.cs +++ /dev/null @@ -1,80 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class DiscordPermOverrideCommands : EllieModule - { - // override stats, it should require that the user has managessages guild permission - // .po 'stats' add user guild managemessages - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverride(CommandOrExprInfo cmd, params GuildPerm[] perms) - { - if (perms is null || perms.Length == 0) - { - await _service.RemoveOverride(ctx.Guild.Id, cmd.Name); - await ReplyConfirmLocalizedAsync(strs.perm_override_reset); - return; - } - - var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc); - await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms); - - await ReplyConfirmLocalizedAsync(strs.perm_override(Format.Bold(aggregatePerms.ToString()), - Format.Code(cmd.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverrideReset() - { - var result = await PromptUserConfirmAsync(_eb.Create() - .WithOkColor() - .WithDescription(GetText(strs.perm_override_all_confirm))); - - if (!result) - return; - - await _service.ClearAllOverrides(ctx.Guild.Id); - - await ReplyConfirmLocalizedAsync(strs.perm_override_all); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverrideList(int page = 1) - { - if (--page < 0) - return; - - var overrides = await _service.GetAllOverrides(ctx.Guild.Id); - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var eb = _eb.Create().WithTitle(GetText(strs.perm_overrides)).WithOkColor(); - - var thisPageOverrides = overrides.Skip(9 * curPage).Take(9).ToList(); - - if (thisPageOverrides.Count == 0) - eb.WithDescription(GetText(strs.perm_override_page_none)); - else - { - eb.WithDescription(thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}") - .Join("\n")); - } - - return eb; - }, - overrides.Count, - 9); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateCommands.cs b/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateCommands.cs deleted file mode 100644 index 4b82dbf..0000000 --- a/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateCommands.cs +++ /dev/null @@ -1,60 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class PlayingRotateCommands : EllieModule - { - [Cmd] - [OwnerOnly] - public async Task RotatePlaying() - { - if (_service.ToggleRotatePlaying()) - await ReplyConfirmLocalizedAsync(strs.ropl_enabled); - else - await ReplyConfirmLocalizedAsync(strs.ropl_disabled); - } - - [Cmd] - [OwnerOnly] - public async Task AddPlaying(ActivityType t, [Leftover] string status) - { - await _service.AddPlaying(t, status); - - await ReplyConfirmLocalizedAsync(strs.ropl_added); - } - - [Cmd] - [OwnerOnly] - public async Task ListPlaying() - { - var statuses = _service.GetRotatingStatuses(); - - if (!statuses.Any()) - await ReplyErrorLocalizedAsync(strs.ropl_not_set); - else - { - var i = 1; - await ReplyConfirmLocalizedAsync(strs.ropl_list(string.Join("\n\t", - statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}")))); - } - } - - [Cmd] - [OwnerOnly] - public async Task RemovePlaying(int index) - { - index -= 1; - - var msg = await _service.RemovePlayingAsync(index); - - if (msg is null) - return; - - await ReplyConfirmLocalizedAsync(strs.reprm(msg)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs b/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs deleted file mode 100644 index 8bce6e5..0000000 --- a/src/Ellie.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs +++ /dev/null @@ -1,109 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public sealed class PlayingRotateService : IEService, IReadyExecutor -{ - private readonly BotConfigService _bss; - private readonly SelfService _selfService; - private readonly Replacer _rep; - private readonly DbService _db; - private readonly DiscordSocketClient _client; - - public PlayingRotateService( - DiscordSocketClient client, - DbService db, - BotConfigService bss, - IEnumerable phProviders, - SelfService selfService) - { - _db = db; - _bss = bss; - _selfService = selfService; - _client = client; - - if (client.ShardId == 0) - _rep = new ReplacementBuilder().WithClient(client).WithProviders(phProviders).Build(); - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); - var index = 0; - while (await timer.WaitForNextTickAsync()) - { - try - { - if (!_bss.Data.RotateStatuses) - continue; - - IReadOnlyList rotatingStatuses; - await using (var uow = _db.GetDbContext()) - { - rotatingStatuses = uow.Set().AsNoTracking().OrderBy(x => x.Id).ToList(); - } - - if (rotatingStatuses.Count == 0) - continue; - - var playingStatus = index >= rotatingStatuses.Count - ? rotatingStatuses[index = 0] - : rotatingStatuses[index++]; - - var statusText = _rep.Replace(playingStatus.Status); - await _selfService.SetGameAsync(statusText, (ActivityType)playingStatus.Type); - } - catch (Exception ex) - { - Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message); - } - } - } - - public async Task RemovePlayingAsync(int index) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - await using var uow = _db.GetDbContext(); - var toRemove = await uow.Set().AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync(); - - if (toRemove is null) - return null; - - uow.Remove(toRemove); - await uow.SaveChangesAsync(); - return toRemove.Status; - } - - public async Task AddPlaying(ActivityType activityType, string status) - { - await using var uow = _db.GetDbContext(); - var toAdd = new RotatingPlayingStatus - { - Status = status, - Type = (Ellie.Bot.Db.ActivityType)activityType - }; - uow.Add(toAdd); - await uow.SaveChangesAsync(); - } - - public bool ToggleRotatePlaying() - { - var enabled = false; - _bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; }); - return enabled; - } - - public IReadOnlyList GetRotatingStatuses() - { - using var uow = _db.GetDbContext(); - return uow.Set().AsNoTracking().ToList(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/PrefixCommands.cs b/src/Ellie.Bot.Modules.Administration/PrefixCommands.cs deleted file mode 100644 index e78c208..0000000 --- a/src/Ellie.Bot.Modules.Administration/PrefixCommands.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class PrefixCommands : EllieModule - { - public enum Set - { - Set - } - - [Cmd] - [Priority(1)] - public async Task Prefix() - => await ReplyConfirmLocalizedAsync(strs.prefix_current(Format.Code(_cmdHandler.GetPrefix(ctx.Guild)))); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public Task Prefix(Set _, [Leftover] string newPrefix) - => Prefix(newPrefix); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public async Task Prefix([Leftover] string toSet) - { - if (string.IsNullOrWhiteSpace(prefix)) - return; - - var oldPrefix = prefix; - var newPrefix = _cmdHandler.SetPrefix(ctx.Guild, toSet); - - await ReplyConfirmLocalizedAsync(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))); - } - - [Cmd] - [OwnerOnly] - public async Task DefPrefix([Leftover] string toSet = null) - { - if (string.IsNullOrWhiteSpace(toSet)) - { - await ReplyConfirmLocalizedAsync(strs.defprefix_current(_cmdHandler.GetPrefix())); - return; - } - - var oldPrefix = _cmdHandler.GetPrefix(); - var newPrefix = _cmdHandler.SetDefaultPrefix(toSet); - - await ReplyConfirmLocalizedAsync(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionCommands.cs b/src/Ellie.Bot.Modules.Administration/Protection/ProtectionCommands.cs deleted file mode 100644 index 7820e68..0000000 --- a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionCommands.cs +++ /dev/null @@ -1,288 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders.Models; -using Ellie.Modules.Administration.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class ProtectionCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt() - { - if (await _service.TryStopAntiAlt(ctx.Guild.Id)) - { - await ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Alt")); - return; - } - - await ReplyConfirmLocalizedAsync(strs.protection_not_running("Anti-Alt")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt( - StoopidTime minAge, - PunishmentAction action, - [Leftover] StoopidTime punishTime = null) - { - var minAgeMinutes = (int)minAge.Time.TotalMinutes; - var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0; - - if (minAgeMinutes < 1 || punishTimeMinutes < 0) - return; - - var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0; - if (action is PunishmentAction.TimeOut && minutes < 1) - minutes = 1; - - await _service.StartAntiAltAsync(ctx.Guild.Id, - minAgeMinutes, - action, - minutes); - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] IRole role) - { - var minAgeMinutes = (int)minAge.Time.TotalMinutes; - - if (minAgeMinutes < 1) - return; - - if (action == PunishmentAction.TimeOut) - return; - - await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id); - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public Task AntiRaid() - { - if (_service.TryStopAntiRaid(ctx.Guild.Id)) - return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Raid")); - return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Raid")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public Task AntiRaid( - int userThreshold, - int seconds, - PunishmentAction action, - [Leftover] StoopidTime punishTime) - => InternalAntiRaid(userThreshold, seconds, action, punishTime); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(2)] - public Task AntiRaid(int userThreshold, int seconds, PunishmentAction action) - => InternalAntiRaid(userThreshold, seconds, action); - - private async Task InternalAntiRaid( - int userThreshold, - int seconds = 10, - PunishmentAction action = PunishmentAction.Mute, - StoopidTime punishTime = null) - { - if (action == PunishmentAction.AddRole) - { - await ReplyErrorLocalizedAsync(strs.punishment_unsupported(action)); - return; - } - - if (userThreshold is < 2 or > 30) - { - await ReplyErrorLocalizedAsync(strs.raid_cnt(2, 30)); - return; - } - - if (seconds is < 2 or > 300) - { - await ReplyErrorLocalizedAsync(strs.raid_time(2, 300)); - return; - } - - if (punishTime is not null) - { - if (!_service.IsDurationAllowed(action)) - await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); - } - - var time = (int?)punishTime?.Time.TotalMinutes ?? 0; - if (time is < 0 or > 60 * 24) - return; - - if(action is PunishmentAction.TimeOut && time < 1) - return; - - var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, action, time); - - if (stats is null) - return; - - await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")), - $"{ctx.User.Mention} {GetAntiRaidString(stats)}"); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public Task AntiSpam() - { - if (_service.TryStopAntiSpam(ctx.Guild.Id)) - return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Spam")); - return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Spam")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] IRole role) - { - if (action != PunishmentAction.AddRole) - return Task.CompletedTask; - - return InternalAntiSpam(messageCount, action, null, role); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime) - => InternalAntiSpam(messageCount, action, punishTime); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(2)] - public Task AntiSpam(int messageCount, PunishmentAction action) - => InternalAntiSpam(messageCount, action); - - private async Task InternalAntiSpam( - int messageCount, - PunishmentAction action, - StoopidTime timeData = null, - IRole role = null) - { - if (messageCount is < 2 or > 10) - return; - - if (timeData is not null) - { - if (!_service.IsDurationAllowed(action)) - await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); - } - - var time = (int?)timeData?.Time.TotalMinutes ?? 0; - if (time is < 0 or > 60 * 24) - return; - - if (action is PunishmentAction.TimeOut && time < 1) - return; - - var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id); - - await SendConfirmAsync(GetText(strs.prot_enable("Anti-Spam")), - $"{ctx.User.Mention} {GetAntiSpamString(stats)}"); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntispamIgnore() - { - var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id); - - if (added is null) - { - await ReplyErrorLocalizedAsync(strs.protection_not_running("Anti-Spam")); - return; - } - - if (added.Value) - await ReplyConfirmLocalizedAsync(strs.spam_ignore("Anti-Spam")); - else - await ReplyConfirmLocalizedAsync(strs.spam_not_ignore("Anti-Spam")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AntiList() - { - var (spam, raid, alt) = _service.GetAntiStats(ctx.Guild.Id); - - if (spam is null && raid is null && alt is null) - { - await ReplyConfirmLocalizedAsync(strs.prot_none); - return; - } - - var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.prot_active)); - - if (spam is not null) - embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true); - - if (raid is not null) - embed.AddField("Anti-Raid", GetAntiRaidString(raid).TrimTo(1024), true); - - if (alt is not null) - embed.AddField("Anti-Alt", GetAntiAltString(alt), true); - - await ctx.Channel.EmbedAsync(embed); - } - - private string GetAntiAltString(AntiAltStats alt) - => GetText(strs.anti_alt_status(Format.Bold(alt.MinAge.ToString(@"dd\d\ hh\h\ mm\m\ ")), - Format.Bold(alt.Action.ToString()), - Format.Bold(alt.Counter.ToString()))); - - private string GetAntiSpamString(AntiSpamStats stats) - { - var settings = stats.AntiSpamSettings; - var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); - - if (string.IsNullOrWhiteSpace(ignoredString)) - ignoredString = "none"; - - var add = string.Empty; - if (settings.MuteTime > 0) - add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})"; - - return GetText(strs.spam_stats(Format.Bold(settings.MessageThreshold.ToString()), - Format.Bold(settings.Action + add), - ignoredString)); - } - - private string GetAntiRaidString(AntiRaidStats stats) - { - var actionString = Format.Bold(stats.AntiRaidSettings.Action.ToString()); - - if (stats.AntiRaidSettings.PunishDuration > 0) - actionString += $" **({TimeSpan.FromMinutes(stats.AntiRaidSettings.PunishDuration):hh\\hmm\\m})**"; - - return GetText(strs.raid_stats(Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()), - Format.Bold(stats.AntiRaidSettings.Seconds.ToString()), - actionString)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionService.cs b/src/Ellie.Bot.Modules.Administration/Protection/ProtectionService.cs deleted file mode 100644 index bee9110..0000000 --- a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionService.cs +++ /dev/null @@ -1,499 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; -using System.Threading.Channels; - -namespace Ellie.Modules.Administration.Services; - -public class ProtectionService : IEService -{ - public event Func OnAntiProtectionTriggered = delegate - { - return Task.CompletedTask; - }; - - private readonly ConcurrentDictionary _antiRaidGuilds = new(); - - private readonly ConcurrentDictionary _antiSpamGuilds = new(); - - private readonly ConcurrentDictionary _antiAltGuilds = new(); - - private readonly DiscordSocketClient _client; - private readonly MuteService _mute; - private readonly DbService _db; - private readonly UserPunishService _punishService; - - private readonly Channel _punishUserQueue = - Channel.CreateUnbounded(new() - { - SingleReader = true, - SingleWriter = false - }); - - public ProtectionService( - DiscordSocketClient client, - IBot bot, - MuteService mute, - DbService db, - UserPunishService punishService) - { - _client = client; - _mute = mute; - _db = db; - _punishService = punishService; - - var ids = client.GetGuildIds(); - using (var uow = db.GetDbContext()) - { - var configs = uow.Set() - .AsQueryable() - .Include(x => x.AntiRaidSetting) - .Include(x => x.AntiSpamSetting) - .ThenInclude(x => x.IgnoredChannels) - .Include(x => x.AntiAltSetting) - .Where(x => ids.Contains(x.GuildId)) - .ToList(); - - foreach (var gc in configs) - Initialize(gc); - } - - _client.MessageReceived += HandleAntiSpam; - _client.UserJoined += HandleUserJoined; - - bot.JoinedGuild += _bot_JoinedGuild; - _client.LeftGuild += _client_LeftGuild; - - _ = Task.Run(RunQueue); - } - - private async Task RunQueue() - { - while (true) - { - var item = await _punishUserQueue.Reader.ReadAsync(); - - var muteTime = item.MuteTime; - var gu = item.User; - try - { - await _punishService.ApplyPunishment(gu.Guild, - gu, - _client.CurrentUser, - item.Action, - muteTime, - item.RoleId, - $"{item.Type} Protection"); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in punish queue: {Message}", ex.Message); - } - finally - { - await Task.Delay(1000); - } - } - } - - private Task _client_LeftGuild(SocketGuild guild) - { - _ = Task.Run(async () => - { - TryStopAntiRaid(guild.Id); - TryStopAntiSpam(guild.Id); - await TryStopAntiAlt(guild.Id); - }); - return Task.CompletedTask; - } - - private Task _bot_JoinedGuild(GuildConfig gc) - { - using var uow = _db.GetDbContext(); - var gcWithData = uow.GuildConfigsForId(gc.GuildId, - set => set.Include(x => x.AntiRaidSetting) - .Include(x => x.AntiAltSetting) - .Include(x => x.AntiSpamSetting) - .ThenInclude(x => x.IgnoredChannels)); - - Initialize(gcWithData); - return Task.CompletedTask; - } - - private void Initialize(GuildConfig gc) - { - var raid = gc.AntiRaidSetting; - var spam = gc.AntiSpamSetting; - - if (raid is not null) - { - var raidStats = new AntiRaidStats - { - AntiRaidSettings = raid - }; - _antiRaidGuilds[gc.GuildId] = raidStats; - } - - if (spam is not null) - { - _antiSpamGuilds[gc.GuildId] = new() - { - AntiSpamSettings = spam - }; - } - - var alt = gc.AntiAltSetting; - if (alt is not null) - _antiAltGuilds[gc.GuildId] = new(alt); - } - - private Task HandleUserJoined(SocketGuildUser user) - { - if (user.IsBot) - return Task.CompletedTask; - - _antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats); - _antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts); - - if (maybeStats is null && maybeAlts is null) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - if (maybeAlts is { } alts) - { - if (user.CreatedAt != default) - { - var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime; - if (diff < alts.MinAge) - { - alts.Increment(); - - await PunishUsers(alts.Action, - ProtectionType.Alting, - alts.ActionDurationMinutes, - alts.RoleId, - user); - - return; - } - } - } - - try - { - if (maybeStats is not { } stats || !stats.RaidUsers.Add(user)) - return; - - ++stats.UsersCount; - - if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold) - { - var users = stats.RaidUsers.ToArray(); - stats.RaidUsers.Clear(); - var settings = stats.AntiRaidSettings; - - await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users); - } - - await Task.Delay(1000 * stats.AntiRaidSettings.Seconds); - - stats.RaidUsers.TryRemove(user); - --stats.UsersCount; - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task HandleAntiSpam(SocketMessage arg) - { - if (arg is not SocketUserMessage msg || msg.Author.IsBot) - return Task.CompletedTask; - - if (msg.Channel is not ITextChannel channel) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - try - { - if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) - || spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new() - { - ChannelId = channel.Id - })) - return; - - var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, - _ => new(msg), - (_, old) => - { - old.ApplyNextMessage(msg); - return old; - }); - - if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) - { - if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) - { - var settings = spamSettings.AntiSpamSettings; - await PunishUsers(settings.Action, - ProtectionType.Spamming, - settings.MuteTime, - settings.RoleId, - (IGuildUser)msg.Author); - } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private async Task PunishUsers( - PunishmentAction action, - ProtectionType pt, - int muteTime, - ulong? roleId, - params IGuildUser[] gus) - { - Log.Information("[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild", - pt, - gus.Length, - action, - gus[0].Guild.Name); - - foreach (var gu in gus) - { - await _punishUserQueue.Writer.WriteAsync(new() - { - Action = action, - Type = pt, - User = gu, - MuteTime = muteTime, - RoleId = roleId - }); - } - - _ = OnAntiProtectionTriggered(action, pt, gus); - } - - public async Task StartAntiRaidAsync( - ulong guildId, - int userThreshold, - int seconds, - PunishmentAction action, - int minutesDuration) - { - var g = _client.GetGuild(guildId); - await _mute.GetMuteRole(g); - - if (action == PunishmentAction.AddRole) - return null; - - if (!IsDurationAllowed(action)) - minutesDuration = 0; - - var stats = new AntiRaidStats - { - AntiRaidSettings = new() - { - Action = action, - Seconds = seconds, - UserThreshold = userThreshold, - PunishDuration = minutesDuration - } - }; - - _antiRaidGuilds.AddOrUpdate(guildId, stats, (_, _) => stats); - - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting)); - - gc.AntiRaidSetting = stats.AntiRaidSettings; - await uow.SaveChangesAsync(); - - return stats; - } - - public bool TryStopAntiRaid(ulong guildId) - { - if (_antiRaidGuilds.TryRemove(guildId, out _)) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting)); - - gc.AntiRaidSetting = null; - uow.SaveChanges(); - return true; - } - - return false; - } - - public bool TryStopAntiSpam(ulong guildId) - { - if (_antiSpamGuilds.TryRemove(guildId, out _)) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); - - gc.AntiSpamSetting = null; - uow.SaveChanges(); - return true; - } - - return false; - } - - public async Task StartAntiSpamAsync( - ulong guildId, - int messageCount, - PunishmentAction action, - int punishDurationMinutes, - ulong? roleId) - { - var g = _client.GetGuild(guildId); - await _mute.GetMuteRole(g); - - if (!IsDurationAllowed(action)) - punishDurationMinutes = 0; - - var stats = new AntiSpamStats - { - AntiSpamSettings = new() - { - Action = action, - MessageThreshold = messageCount, - MuteTime = punishDurationMinutes, - RoleId = roleId - } - }; - - stats = _antiSpamGuilds.AddOrUpdate(guildId, - stats, - (_, old) => - { - stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels; - return stats; - }); - - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting)); - - if (gc.AntiSpamSetting is not null) - { - gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action; - gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold; - gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime; - gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId; - } - else - gc.AntiSpamSetting = stats.AntiSpamSettings; - - await uow.SaveChangesAsync(); - return stats; - } - - public async Task AntiSpamIgnoreAsync(ulong guildId, ulong channelId) - { - var obj = new AntiSpamIgnore - { - ChannelId = channelId - }; - bool added; - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); - var spam = gc.AntiSpamSetting; - if (spam is null) - return null; - - if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful - { - if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache - - added = true; - } - else - { - var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId); - uow.Set().Remove(toRemove); // remove from db - if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache - - added = false; - } - - await uow.SaveChangesAsync(); - return added; - } - - public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId) - { - _antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats); - _antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats); - _antiAltGuilds.TryGetValue(guildId, out var antiAltStats); - - return (antiSpamStats, antiRaidStats, antiAltStats); - } - - public bool IsDurationAllowed(PunishmentAction action) - { - switch (action) - { - case PunishmentAction.Ban: - case PunishmentAction.Mute: - case PunishmentAction.ChatMute: - case PunishmentAction.VoiceMute: - case PunishmentAction.AddRole: - case PunishmentAction.TimeOut: - return true; - default: - return false; - } - } - - public async Task StartAntiAltAsync( - ulong guildId, - int minAgeMinutes, - PunishmentAction action, - int actionDurationMinutes = 0, - ulong? roleId = null) - { - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); - gc.AntiAltSetting = new() - { - Action = action, - ActionDurationMinutes = actionDurationMinutes, - MinAge = TimeSpan.FromMinutes(minAgeMinutes), - RoleId = roleId - }; - - await uow.SaveChangesAsync(); - _antiAltGuilds[guildId] = new(gc.AntiAltSetting); - } - - public async Task TryStopAntiAlt(ulong guildId) - { - if (!_antiAltGuilds.TryRemove(guildId, out _)) - return false; - - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); - gc.AntiAltSetting = null; - await uow.SaveChangesAsync(); - return true; - } -} diff --git a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionStats.cs b/src/Ellie.Bot.Modules.Administration/Protection/ProtectionStats.cs deleted file mode 100644 index 7f4684d..0000000 --- a/src/Ellie.Bot.Modules.Administration/Protection/ProtectionStats.cs +++ /dev/null @@ -1,52 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public enum ProtectionType -{ - Raiding, - Spamming, - Alting -} - -public class AntiRaidStats -{ - public AntiRaidSetting AntiRaidSettings { get; set; } - public int UsersCount { get; set; } - public ConcurrentHashSet RaidUsers { get; set; } = new(); -} - -public class AntiSpamStats -{ - public AntiSpamSetting AntiSpamSettings { get; set; } - public ConcurrentDictionary UserStats { get; set; } = new(); -} - -public class AntiAltStats -{ - public PunishmentAction Action - => _setting.Action; - - public int ActionDurationMinutes - => _setting.ActionDurationMinutes; - - public ulong? RoleId - => _setting.RoleId; - - public TimeSpan MinAge - => _setting.MinAge; - - public int Counter - => counter; - - private readonly AntiAltSetting _setting; - - private int counter; - - public AntiAltStats(AntiAltSetting setting) - => _setting = setting; - - public void Increment() - => Interlocked.Increment(ref counter); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Protection/PunishQueueItem.cs b/src/Ellie.Bot.Modules.Administration/Protection/PunishQueueItem.cs deleted file mode 100644 index 59e5e69..0000000 --- a/src/Ellie.Bot.Modules.Administration/Protection/PunishQueueItem.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public class PunishQueueItem -{ - public PunishmentAction Action { get; set; } - public ProtectionType Type { get; set; } - public int MuteTime { get; set; } - public ulong? RoleId { get; set; } - public IGuildUser User { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Protection/UserSpamStats.cs b/src/Ellie.Bot.Modules.Administration/Protection/UserSpamStats.cs deleted file mode 100644 index 09a9be0..0000000 --- a/src/Ellie.Bot.Modules.Administration/Protection/UserSpamStats.cs +++ /dev/null @@ -1,64 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Administration; - -public sealed class UserSpamStats -{ - public int Count - { - get - { - lock (_applyLock) - { - Cleanup(); - return _messageTracker.Count; - } - } - } - - private string lastMessage; - - private readonly Queue _messageTracker; - - private readonly object _applyLock = new(); - - private readonly TimeSpan _maxTime = TimeSpan.FromMinutes(30); - - public UserSpamStats(IUserMessage msg) - { - lastMessage = msg.Content.ToUpperInvariant(); - _messageTracker = new(); - - ApplyNextMessage(msg); - } - - public void ApplyNextMessage(IUserMessage message) - { - var upperMsg = message.Content.ToUpperInvariant(); - - lock (_applyLock) - { - if (upperMsg != lastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any())) - { - // if it's a new message, reset spam counter - lastMessage = upperMsg; - _messageTracker.Clear(); - } - - _messageTracker.Enqueue(DateTime.UtcNow); - } - } - - private void Cleanup() - { - lock (_applyLock) - { - while (_messageTracker.TryPeek(out var dateTime)) - { - if (DateTime.UtcNow - dateTime < _maxTime) - break; - - _messageTracker.Dequeue(); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Prune/PruneCommands.cs b/src/Ellie.Bot.Modules.Administration/Prune/PruneCommands.cs deleted file mode 100644 index 17e9791..0000000 --- a/src/Ellie.Bot.Modules.Administration/Prune/PruneCommands.cs +++ /dev/null @@ -1,114 +0,0 @@ -#nullable disable -using CommandLine; -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class PruneCommands : EllieModule - { - private static readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); - - public sealed class PruneOptions : IEllieCommandOptions - { - [Option(shortName: 's', longName: "safe", Default = false, HelpText = "Whether pinned messages should be deleted.", Required = false)] - public bool Safe { get; set; } - - [Option(shortName: 'a', longName: "after", Default = null, HelpText = "Prune only messages after the specified message ID.", Required = false)] - public ulong? After { get; set; } - - public void NormalizeOptions() - { - } - } - - //deletes her own messages, no perm required - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public async Task Prune(params string[] args) - { - var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); - - var user = await ctx.Guild.GetCurrentUserAsync(); - - if (opts.Safe) - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After); - else - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After); - - ctx.Message.DeleteAfter(3); - } - - // prune x - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [EllieOptions] - [Priority(1)] - public async Task Prune(int count, params string[] args) - { - count++; - if (count < 1) - return; - if (count > 1000) - count = 1000; - - var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); - - if (opts.Safe) - await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After); - else - await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After); - } - - //prune @user [x] - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [EllieOptions] - [Priority(0)] - public Task Prune(IGuildUser user, int count = 100, params string[] args) - => Prune(user.Id, count, args); - - //prune userid [x] - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [EllieOptions] - [Priority(0)] - public async Task Prune(ulong userId, int count = 100, params string[] args) - { - if (userId == ctx.User.Id) - count++; - - if (count < 1) - return; - - if (count > 1000) - count = 1000; - - var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); - - if (opts.Safe) - { - await _service.PruneWhere((ITextChannel)ctx.Channel, - count, - m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned, - opts.After); - } - else - { - await _service.PruneWhere((ITextChannel)ctx.Channel, - count, - m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks, - opts.After); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Prune/PruneService.cs b/src/Ellie.Bot.Modules.Administration/Prune/PruneService.cs deleted file mode 100644 index bc5e765..0000000 --- a/src/Ellie.Bot.Modules.Administration/Prune/PruneService.cs +++ /dev/null @@ -1,89 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Administration.Services; - -public class PruneService : IEService -{ - //channelids where prunes are currently occuring - private readonly ConcurrentHashSet _pruningGuilds = new(); - private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); - private readonly ILogCommandService _logService; - - public PruneService(ILogCommandService logService) - => _logService = logService; - - public async Task PruneWhere(ITextChannel channel, int amount, Func predicate, ulong? after = null) - { - ArgumentNullException.ThrowIfNull(channel, nameof(channel)); - - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (!_pruningGuilds.Add(channel.GuildId)) - return; - - try - { - var now = DateTime.UtcNow; - IMessage[] msgs; - IMessage lastMessage = null; - var dled = await channel.GetMessagesAsync(50).FlattenAsync(); - - msgs = dled - .Where(predicate) - .Where(x => after is ulong a ? x.Id > a : true) - .Take(amount) - .ToArray(); - - while (amount > 0 && msgs.Any()) - { - lastMessage = msgs[^1]; - - var bulkDeletable = new List(); - var singleDeletable = new List(); - foreach (var x in msgs) - { - _logService.AddDeleteIgnore(x.Id); - - if (now - x.CreatedAt < _twoWeeks) - bulkDeletable.Add(x); - else - singleDeletable.Add(x); - } - - if (bulkDeletable.Count > 0) - { - await channel.DeleteMessagesAsync(bulkDeletable); - await Task.Delay(2000); - } - - foreach (var group in singleDeletable.Chunk(5)) - { - 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 - amount -= 50; - if (amount > 0) - { - dled = await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync(); - - msgs = dled - .Where(predicate) - .Where(x => after is ulong a ? x.Id > a : true) - .Take(amount) - .ToArray(); - } - } - } - catch - { - //ignore - } - finally - { - _pruningGuilds.TryRemove(channel.GuildId); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Role/IReactionRoleService.cs b/src/Ellie.Bot.Modules.Administration/Role/IReactionRoleService.cs deleted file mode 100644 index c4d2b97..0000000 --- a/src/Ellie.Bot.Modules.Administration/Role/IReactionRoleService.cs +++ /dev/null @@ -1,52 +0,0 @@ -#nullable disable -using Ellie.Modules.Patronage; -using Ellie.Services.Database.Models; -using OneOf; -using OneOf.Types; - -namespace Ellie.Modules.Administration.Services; - -public interface IReactionRoleService -{ - /// - /// Adds a single reaction role - /// - /// Guild where to add a reaction role - /// Message to which to add a reaction role - /// - /// - /// - /// - /// The result of the operation - Task> AddReactionRole( - IGuild guild, - IMessage msg, - string emote, - IRole role, - int group = 0, - int levelReq = 0); - - /// - /// Get all reaction roles on the specified server - /// - /// - /// - Task> GetReactionRolesAsync(ulong guildId); - - /// - /// Remove reaction roles on the specified message - /// - /// - /// - /// - Task RemoveReactionRoles(ulong guildId, ulong messageId); - - /// - /// Remove all reaction roles in the specified server - /// - /// - /// - Task RemoveAllReactionRoles(ulong guildId); - - Task> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId); -} diff --git a/src/Ellie.Bot.Modules.Administration/Role/ReactionRoleCommands.cs b/src/Ellie.Bot.Modules.Administration/Role/ReactionRoleCommands.cs deleted file mode 100644 index a679aeb..0000000 --- a/src/Ellie.Bot.Modules.Administration/Role/ReactionRoleCommands.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - public partial class ReactionRoleCommands : EllieModule - { - private readonly IReactionRoleService _rero; - - public ReactionRoleCommands(IReactionRoleService rero) - { - _rero = rero; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ReactionRoleAdd( - ulong messageId, - string emoteStr, - IRole role, - int group = 0, - int levelReq = 0) - { - if (group < 0) - return; - - if (levelReq < 0) - return; - - var msg = await ctx.Channel.GetMessageAsync(messageId); - if (msg is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position) - { - await ReplyErrorLocalizedAsync(strs.hierarchy); - return; - } - - var emote = emoteStr.ToIEmote(); - await msg.AddReactionAsync(emote); - var res = await _rero.AddReactionRole(ctx.Guild, - msg, - emoteStr, - role, - group, - levelReq); - - 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)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ReactionRolesList(int page = 1) - { - if (--page < 0) - return; - - var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id); - - await ctx.SendPaginatedConfirmAsync(page, curPage => - { - var embed = _eb.Create(ctx) - .WithOkColor(); - - var content = string.Empty; - foreach (var g in reros.OrderBy(x => x.Group) - .Skip(curPage * 10) - .GroupBy(x => x.MessageId) - .OrderBy(x => x.Key)) - { - var messageId = g.Key; - content += - $"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n"; - - var groupGroups = g.GroupBy(x => x.Group); - - foreach (var ggs in groupGroups) - { - content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n"; - - foreach (var rero in ggs) - { - content += - $"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "")}"; - if (rero.LevelReq > 0) - content += $" (lvl {rero.LevelReq}+)"; - content += '\n'; - } - } - - } - - embed.WithDescription(string.IsNullOrWhiteSpace(content) - ? "There are no reaction roles on this server" - : content); - - return embed; - }, reros.Count, 10); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ReactionRolesRemove(ulong messageId) - { - var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId); - if (succ) - await ctx.OkAsync(); - else - await ctx.ErrorAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ReactionRolesDeleteAll() - { - await _rero.RemoveAllReactionRoles(ctx.Guild.Id); - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Ratelimit(60)] - public async Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId) - { - var msg = await ctx.Channel.GetMessageAsync(toMessageId); - - if (msg is null) - { - await ctx.ErrorAsync(); - return; - } - - var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId); - - if (reactions.Count == 0) - { - await ctx.ErrorAsync(); - } - else - { - foreach (var r in reactions) - { - await msg.AddReactionAsync(r); - } - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Role/ReactionRolesService.cs b/src/Ellie.Bot.Modules.Administration/Role/ReactionRolesService.cs deleted file mode 100644 index cdb0d25..0000000 --- a/src/Ellie.Bot.Modules.Administration/Role/ReactionRolesService.cs +++ /dev/null @@ -1,396 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Modules.Patronage; -using Ellie.Services.Database.Models; -using OneOf.Types; -using OneOf; - -namespace Ellie.Modules.Administration.Services; - -public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionRoleService -{ - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly IBotCredentials _creds; - - private ConcurrentDictionary> _cache; - private readonly object _cacheLock = new(); - private readonly SemaphoreSlim _assignementLock = new(1, 1); - private readonly IPatronageService _ps; - - 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)) - .ToListAsyncLinqToDB(); - - foreach (var group in reros.GroupBy(x => x.MessageId)) - { - _cache[group.Key] = group.ToList(); - } - - _client.ReactionAdded += ClientOnReactionAdded; - _client.ReactionRemoved += ClientOnReactionRemoved; - } - - private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync( - ulong userId, - ReactionRoleV2 rero) - { - var guild = _client.GetGuild(rero.GuildId); - var role = guild?.GetRole(rero.RoleId); - - if (role is null) - return default; - - var user = guild.GetUser(userId) as IGuildUser - ?? await _client.Rest.GetGuildUserAsync(guild.Id, userId); - - if (user is null) - return default; - - return (user, role); - } - - private Task ClientOnReactionRemoved( - Cacheable cmsg, - Cacheable ch, - SocketReaction r) - { - if (!_cache.TryGetValue(cmsg.Id, out var reros)) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - var emote = await GetFixedEmoteAsync(cmsg, r.Emote); - - var rero = reros.FirstOrDefault(x => x.Emote == emote.Name - || x.Emote == emote.ToString()); - if (rero is null) - return; - - var (user, role) = await GetUserAndRoleAsync(r.UserId, rero); - - if (user.IsBot) - return; - - await _assignementLock.WaitAsync(); - try - { - if (user.RoleIds.Contains(role.Id)) - { - await user.RemoveRoleAsync(role.Id); - } - } - finally - { - _assignementLock.Release(); - } - }); - - return Task.CompletedTask; - } - - - // had to add this because for some reason, reactionremoved event's reaction doesn't have IsAnimated set, - // causing the .ToString() to be wrong on animated custom emotes - private async Task GetFixedEmoteAsync( - Cacheable cmsg, - IEmote inputEmote) - { - // this should only run for emote - if (inputEmote is not Emote e) - return inputEmote; - - // try to get the message and pull - var msg = await cmsg.GetOrDownloadAsync(); - - var emote = msg.Reactions.Keys.FirstOrDefault(x => e.Equals(x)); - return emote ?? inputEmote; - } - - private Task ClientOnReactionAdded( - Cacheable msg, - Cacheable ch, - SocketReaction r) - { - if (!_cache.TryGetValue(msg.Id, out var reros)) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString()); - if (rero is null) - return; - - var (user, role) = await GetUserAndRoleAsync(r.UserId, rero); - - if (user.IsBot) - return; - - await _assignementLock.WaitAsync(); - try - { - if (!user.RoleIds.Contains(role.Id)) - { - // first check if there is a level requirement - // and if there is, make sure user satisfies it - if (rero.LevelReq > 0) - { - await using var ctx = _db.GetDbContext(); - var levelData = await ctx.GetTable() - .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) - { - var exclusive = reros - .Where(x => x.Group == rero.Group && x.RoleId != role.Id) - .Select(x => x.RoleId) - .Distinct(); - - - try { await user.RemoveRolesAsync(exclusive); } - catch { } - - // remove user's previous reaction - try - { - var m = await msg.GetOrDownloadAsync(); - if (m is not null) - { - var reactToRemove = m.Reactions - .FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString()) - .Key; - - if (reactToRemove is not null) - { - await m.RemoveReactionAsync(reactToRemove, user); - } - } - } - catch - { - } - } - - await user.AddRoleAsync(role.Id); - } - } - finally - { - _assignementLock.Release(); - } - }); - - return Task.CompletedTask; - } - - /// - /// Adds a single reaction role - /// - /// Guild where to add a reaction role - /// Message to which to add a reaction role - /// - /// - /// - /// - /// The result of the operation - public async Task> AddReactionRole( - IGuild guild, - IMessage msg, - string emote, - IRole role, - int group = 0, - int levelReq = 0) - { - if (group < 0) - throw new ArgumentOutOfRangeException(nameof(group)); - - if (levelReq < 0) - 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 == guild.Id) - .CountAsync(); - - var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50); - if (result.Quota != -1 && activeReactionRoles >= result.Quota) - return result; - - await ctx.GetTable() - .InsertOrUpdateAsync(() => new() - { - GuildId = guild.Id, - ChannelId = msg.Channel.Id, - - 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, - }); - - await tran.CommitAsync(); - - var obj = new ReactionRoleV2() - { - GuildId = guild.Id, - MessageId = msg.Id, - Emote = emote, - RoleId = role.Id, - Group = group, - LevelReq = levelReq - }; - - lock (_cacheLock) - { - _cache.AddOrUpdate(msg.Id, - _ => new() - { - obj - }, - (_, list) => - { - list.RemoveAll(x => x.Emote == emote); - list.Add(obj); - return list; - }); - } - - return new Success(); - } - - /// - /// Get all reaction roles on the specified server - /// - /// - /// - public async Task> GetReactionRolesAsync(ulong guildId) - { - await using var ctx = _db.GetDbContext(); - return await ctx.GetTable() - .Where(x => x.GuildId == guildId) - .ToListAsync(); - } - - /// - /// Remove reaction roles on the specified message - /// - /// - /// - /// - public async Task RemoveReactionRoles(ulong guildId, ulong messageId) - { - // guildid is used for quick index lookup - await using var ctx = _db.GetDbContext(); - var changed = await ctx.GetTable() - .Where(x => x.GuildId == guildId && x.MessageId == messageId) - .DeleteAsync(); - - _cache.TryRemove(messageId, out _); - - if (changed == 0) - return false; - - return true; - } - - /// - /// Remove all reaction roles in the specified server - /// - /// - /// - public async Task RemoveAllReactionRoles(ulong guildId) - { - await using var ctx = _db.GetDbContext(); - var output = await ctx.GetTable() - .Where(x => x.GuildId == guildId) - .DeleteWithOutputAsync(x => x.MessageId); - - lock (_cacheLock) - { - foreach (var o in output) - { - _cache.TryRemove(o, out _); - } - } - - return output.Length; - } - - public async Task> TransferReactionRolesAsync( - ulong guildId, - ulong fromMessageId, - ulong toMessageId) - { - await using var ctx = _db.GetDbContext(); - var updated = ctx.GetTable() - .Where(x => x.GuildId == guildId && x.MessageId == fromMessageId) - .UpdateWithOutput(old => new() - { - MessageId = toMessageId - }, - (old, neu) => neu); - lock (_cacheLock) - { - if (_cache.TryRemove(fromMessageId, out var data)) - { - if (_cache.TryGetValue(toMessageId, out var newData)) - { - newData.AddRange(data); - } - else - { - _cache[toMessageId] = data; - } - } - } - - return updated.Select(x => x.Emote.ToIEmote()).ToList(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Role/RoleCommands.cs b/src/Ellie.Bot.Modules.Administration/Role/RoleCommands.cs deleted file mode 100644 index 67de9bc..0000000 --- a/src/Ellie.Bot.Modules.Administration/Role/RoleCommands.cs +++ /dev/null @@ -1,184 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp.PixelFormats; -using Color = SixLabors.ImageSharp.Color; - -namespace Ellie.Modules.Administration; - - -public partial class Administration -{ - public partial class RoleCommands : EllieModule - { - public enum Exclude { Excl } - - private readonly IServiceProvider _services; - - public RoleCommands(IServiceProvider services) - { - _services = services; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd) - { - var runnerUser = (IGuildUser)ctx.User; - var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position); - if (ctx.User.Id != ctx.Guild.OwnerId && runnerMaxRolePosition <= roleToAdd.Position) - return; - try - { - await targetUser.AddRoleAsync(roleToAdd); - - await ReplyConfirmLocalizedAsync(strs.setrole(Format.Bold(roleToAdd.Name), - Format.Bold(targetUser.ToString()))); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in setrole command"); - await ReplyErrorLocalizedAsync(strs.setrole_err); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove) - { - var runnerUser = (IGuildUser)ctx.User; - if (ctx.User.Id != runnerUser.Guild.OwnerId - && runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position) - return; - try - { - await targetUser.RemoveRoleAsync(roleToRemove); - await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name), - Format.Bold(targetUser.ToString()))); - } - catch - { - await ReplyErrorLocalizedAsync(strs.remrole_err); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RenameRole(IRole roleToEdit, [Leftover] string newname) - { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position) - return; - try - { - if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync()).GetRoles().Max(r => r.Position)) - { - await ReplyErrorLocalizedAsync(strs.renrole_perms); - return; - } - - await roleToEdit.ModifyAsync(g => g.Name = newname); - await ReplyConfirmLocalizedAsync(strs.renrole); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.renrole_err); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RemoveAllRoles([Leftover] IGuildUser user) - { - var guser = (IGuildUser)ctx.User; - - var userRoles = user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole).ToList(); - - if (user.Id == ctx.Guild.OwnerId - || (ctx.User.Id != ctx.Guild.OwnerId - && guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position))) - return; - try - { - await user.RemoveRolesAsync(userRoles); - await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString()))); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.rar_err); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task CreateRole([Leftover] string roleName = null) - { - if (string.IsNullOrWhiteSpace(roleName)) - return; - - var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false); - await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task DeleteRole([Leftover] IRole role) - { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - await role.DeleteAsync(); - await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RoleHoist([Leftover] IRole role) - { - var newHoisted = !role.IsHoisted; - await role.ModifyAsync(r => r.Hoist = newHoisted); - if (newHoisted) - await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name))); - else - await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task RoleColor([Leftover] IRole role) - => await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6")); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task RoleColor(Color color, [Leftover] IRole role) - { - try - { - var rgba32 = color.ToPixel(); - await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B)); - await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name))); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.rc_perms); - } - } - } -} diff --git a/src/Ellie.Bot.Modules.Administration/Self/CheckForUpdatesService.cs b/src/Ellie.Bot.Modules.Administration/Self/CheckForUpdatesService.cs deleted file mode 100644 index f121f8c..0000000 --- a/src/Ellie.Bot.Modules.Administration/Self/CheckForUpdatesService.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.Net.Http.Json; -using System.Text; -using Ellie.Common.ModuleBehaviors; - -namespace Ellie.Modules.Administration.Self; - -public sealed class CheckForUpdatesService : IEService, IReadyExecutor -{ - private readonly BotConfigService _bcs; - private readonly IBotCredsProvider _bcp; - private readonly IHttpClientFactory _httpFactory; - private readonly DiscordSocketClient _client; - private readonly IEmbedBuilderService _ebs; - - public CheckForUpdatesService(BotConfigService bcs, IBotCredsProvider bcp, IHttpClientFactory httpFactory, - DiscordSocketClient client, IEmbedBuilderService ebs) - { - _bcs = bcs; - _bcp = bcp; - _httpFactory = httpFactory; - _client = client; - _ebs = ebs; - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - while (await timer.WaitForNextTickAsync()) - { - var conf = _bcs.Data; - - if (!conf.CheckForUpdates) - continue; - - try - { - const string URL = "https://cdn.elliebot.net/cmds/versions.json"; - using var http = _httpFactory.CreateClient(); - var versions = await http.GetFromJsonAsync(URL); - - if (versions is null) - continue; - - var latest = versions[0]; - var latestVersion = Version.Parse(latest); - var lastKnownVersion = GetLastKnownVersion(); - - if (lastKnownVersion is null) - { - UpdateLastKnownVersion(latestVersion); - continue; - } - - if (latestVersion > lastKnownVersion) - { - UpdateLastKnownVersion(latestVersion); - - // pull changelog - var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/main/CHANGELOG.md"); - - var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog); - - if (string.IsNullOrWhiteSpace(thisVersionChangelog)) - { - Log.Warning("New version {BotVersion} was found but changelog is unavailable", - thisVersionChangelog); - continue; - } - - var creds = _bcp.GetCreds(); - await creds.OwnerIds - .Select(async x => - { - var user = await _client.GetUserAsync(x); - if (user is null) - return; - - var eb = _ebs.Create() - .WithOkColor() - .WithAuthor($"Ellie v{latestVersion} Released!") - .WithTitle("Changelog") - .WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/main/CHANGELOG.md") - .WithDescription(thisVersionChangelog.TrimTo(4096)) - .WithFooter("You may disable these messages by typing '.conf bot checkforupdates false'"); - - await user.EmbedAsync(eb); - }).WhenAll(); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error while checking for new bot release: {ErrorMessage}", ex.Message); - } - } - } - - private string? GetVersionChangelog(Version latestVersion, string changelog) - { - var clSpan = changelog.AsSpan(); - - var sb = new StringBuilder(); - var started = false; - foreach (var line in clSpan.EnumerateLines()) - { - // if we're at the current version, keep reading lines and adding to the output - if (started) - { - // if we got to previous version, end - if (line.StartsWith("## [")) - break; - - // if we're reading a new segment, reformat it to print it better to discord - if (line.StartsWith("### ")) - { - sb.AppendLine(Format.Bold(line.ToString())); - } - else - { - sb.AppendLine(line.ToString()); - } - - continue; - } - - if (line.StartsWith($"## [{latestVersion.ToString()}]")) - { - started = true; - continue; - } - } - - return sb.ToString(); - } - - private const string LAST_KNOWN_VERSION_PATH = "data/last_known_version.txt"; - private Version? GetLastKnownVersion() - { - if (!File.Exists(LAST_KNOWN_VERSION_PATH)) - return null; - - return Version.TryParse(File.ReadAllText(LAST_KNOWN_VERSION_PATH), out var ver) - ? ver - : null; - } - - private void UpdateLastKnownVersion(Version version) - { - File.WriteAllText("data/last_known_version.txt", version.ToString()); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Self/SelfCommands.cs b/src/Ellie.Bot.Modules.Administration/Self/SelfCommands.cs deleted file mode 100644 index a03c8ce..0000000 --- a/src/Ellie.Bot.Modules.Administration/Self/SelfCommands.cs +++ /dev/null @@ -1,572 +0,0 @@ -#nullable disable -using Ellie.Marmalade; -using Ellie.Modules.Administration.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class SelfCommands : EllieModule - { - public enum SettableUserStatus - { - Online, - Invisible, - Idle, - Dnd - } - - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; - private readonly IMarmaladeLoaderSevice _marmaladeLoader; - private readonly ICoordinator _coord; - - public SelfCommands( - DiscordSocketClient client, - IBotStrings strings, - ICoordinator coord, - IMarmaladeLoaderSevice marmaladeLoader) - { - _client = client; - _strings = strings; - _coord = coord; - _marmaladeLoader = marmaladeLoader; - } - - [Cmd] - [OwnerOnly] - public async Task DoAs(IUser user, [Leftover] string message) - { - if (ctx.User is not IGuildUser { GuildPermissions.Administrator: true }) - return; - - if (ctx.Guild is SocketGuild sg && ctx.Channel is ISocketMessageChannel ch - && ctx.Message is SocketUserMessage msg) - { - var fakeMessage = new DoAsUserMessage(msg, user, message); - - await _cmdHandler.TryRunCommand(sg, ch, fakeMessage); - } - else - { - await ReplyErrorLocalizedAsync(strs.error_occured); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task StartupCommandAdd([Leftover] string cmdText) - { - if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture)) - return; - - var guser = (IGuildUser)ctx.User; - var cmd = new AutoCommand - { - CommandText = cmdText, - ChannelId = ctx.Channel.Id, - ChannelName = ctx.Channel.Name, - GuildId = ctx.Guild?.Id, - GuildName = ctx.Guild?.Name, - VoiceChannelId = guser.VoiceChannel?.Id, - VoiceChannelName = guser.VoiceChannel?.Name, - Interval = 0 - }; - _service.AddNewAutoCommand(cmd); - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.scadd)) - .AddField(GetText(strs.server), - cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}", - true) - .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) - .AddField(GetText(strs.command_text), cmdText)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task AutoCommandAdd(int interval, [Leftover] string cmdText) - { - if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture)) - return; - - if (interval < 5) - return; - - var guser = (IGuildUser)ctx.User; - var cmd = new AutoCommand - { - CommandText = cmdText, - ChannelId = ctx.Channel.Id, - ChannelName = ctx.Channel.Name, - GuildId = ctx.Guild?.Id, - GuildName = ctx.Guild?.Name, - VoiceChannelId = guser.VoiceChannel?.Id, - VoiceChannelName = guser.VoiceChannel?.Name, - Interval = interval - }; - _service.AddNewAutoCommand(cmd); - - await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task StartupCommandsList(int page = 1) - { - if (page-- < 1) - return; - - var scmds = _service.GetStartupCommands().Skip(page * 5).Take(5).ToList(); - - if (scmds.Count == 0) - await ReplyErrorLocalizedAsync(strs.startcmdlist_none); - else - { - var i = 0; - await SendConfirmAsync(text: string.Join("\n", - scmds.Select(x => $@"```css -#{++i + (page * 5)} -[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")} -[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId} -[{GetText(strs.command_text)}]: {x.CommandText}```")), - title: string.Empty, - footer: GetText(strs.page(page + 1))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task AutoCommandsList(int page = 1) - { - if (page-- < 1) - return; - - var scmds = _service.GetAutoCommands().Skip(page * 5).Take(5).ToList(); - if (!scmds.Any()) - await ReplyErrorLocalizedAsync(strs.autocmdlist_none); - else - { - var i = 0; - await SendConfirmAsync(text: string.Join("\n", - scmds.Select(x => $@"```css -#{++i + (page * 5)} -[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")} -[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId} -{GetIntervalText(x.Interval)} -[{GetText(strs.command_text)}]: {x.CommandText}```")), - title: string.Empty, - footer: GetText(strs.page(page + 1))); - } - } - - private string GetIntervalText(int interval) - => $"[{GetText(strs.interval)}]: {interval}"; - - [Cmd] - [OwnerOnly] - public async Task Wait(int miliseconds) - { - if (miliseconds <= 0) - return; - ctx.Message.DeleteAfter(0); - try - { - var msg = await SendConfirmAsync($"⏲ {miliseconds}ms"); - msg.DeleteAfter(miliseconds / 1000); - } - catch { } - - await Task.Delay(miliseconds); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task AutoCommandRemove([Leftover] int index) - { - if (!_service.RemoveAutoCommand(--index, out _)) - { - await ReplyErrorLocalizedAsync(strs.acrm_fail); - return; - } - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task StartupCommandRemove([Leftover] int index) - { - if (!_service.RemoveStartupCommand(--index, out _)) - await ReplyErrorLocalizedAsync(strs.scrm_fail); - else - await ReplyConfirmLocalizedAsync(strs.scrm); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task StartupCommandsClear() - { - _service.ClearStartupCommands(); - - await ReplyConfirmLocalizedAsync(strs.startcmds_cleared); - } - - [Cmd] - [OwnerOnly] - public async Task ForwardMessages() - { - var enabled = _service.ForwardMessages(); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.fwdm_start); - else - await ReplyPendingLocalizedAsync(strs.fwdm_stop); - } - - [Cmd] - [OwnerOnly] - public async Task ForwardToAll() - { - var enabled = _service.ForwardToAll(); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.fwall_start); - else - await ReplyPendingLocalizedAsync(strs.fwall_stop); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task ForwardToChannel() - { - var enabled = _service.ForwardToChannel(ctx.Channel.Id); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.fwch_start); - else - await ReplyPendingLocalizedAsync(strs.fwch_stop); - } - - [Cmd] - public async Task ShardStats(int page = 1) - { - if (--page < 0) - return; - - var statuses = _coord.GetAllShardStatuses(); - - var status = string.Join(" : ", - statuses.Select(x => (ConnectionStateToEmoji(x), x)) - .GroupBy(x => x.Item1) - .Select(x => $"`{x.Count()} {x.Key}`") - .ToArray()); - - var allShardStrings = statuses.Select(st => - { - var timeDiff = DateTime.UtcNow - st.LastUpdate; - var stateStr = ConnectionStateToEmoji(st); - var maxGuildCountLength = - statuses.Max(x => x.GuildCount).ToString().Length; - return $"`{stateStr} " - + $"| #{st.ShardId.ToString().PadBoth(3)} " - + $"| {timeDiff:mm\\:ss} " - + $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `"; - }) - .ToArray(); - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); - - if (string.IsNullOrWhiteSpace(str)) - str = GetText(strs.no_shards_on_page); - - return _eb.Create().WithOkColor().WithDescription($"{status}\n\n{str}"); - }, - allShardStrings.Length, - 25); - } - - private static string ConnectionStateToEmoji(ShardStatus status) - { - var timeDiff = DateTime.UtcNow - status.LastUpdate; - return status.ConnectionState switch - { - ConnectionState.Disconnected => "🔻", - _ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ", - ConnectionState.Connected => "✅", - _ => " ⏳" - }; - } - - [Cmd] - [OwnerOnly] - public async Task RestartShard(int shardId) - { - var success = _coord.RestartShard(shardId); - if (success) - await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId))); - else - await ReplyErrorLocalizedAsync(strs.no_shard_id); - } - - [Cmd] - [OwnerOnly] - public Task Leave([Leftover] string guildStr) - => _service.LeaveGuild(guildStr); - - [Cmd] - [OwnerOnly] - public async Task DeleteEmptyServers() - { - await ctx.Channel.TriggerTypingAsync(); - - var toLeave = _client.Guilds - .Where(s => s.MemberCount == 1 && s.Users.Count == 1) - .ToList(); - - foreach (var server in toLeave) - { - try - { - await server.DeleteAsync(); - Log.Information("Deleted server {ServerName} [{ServerId}]", - server.Name, - server.Id); - } - catch (Exception ex) - { - Log.Warning(ex, - "Error leaving server {ServerName} [{ServerId}]", - server.Name, - server.Id); - } - } - - await ReplyConfirmLocalizedAsync(strs.deleted_x_servers(toLeave.Count)); - } - - [Cmd] - [OwnerOnly] - public async Task Die(bool graceful = false) - { - try - { - await _client.SetStatusAsync(UserStatus.Invisible); - _ = _client.StopAsync(); - await ReplyConfirmLocalizedAsync(strs.shutting_down); - } - catch - { - // ignored - } - - await Task.Delay(2000); - _coord.Die(graceful); - } - - [Cmd] - [OwnerOnly] - public async Task Restart() - { - var success = _coord.RestartBot(); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.restart_fail); - return; - } - - try { await ReplyConfirmLocalizedAsync(strs.restarting); } - catch { } - } - - [Cmd] - [OwnerOnly] - public async Task SetName([Leftover] string newName) - { - if (string.IsNullOrWhiteSpace(newName)) - return; - - try - { - await _client.CurrentUser.ModifyAsync(u => u.Username = newName); - } - catch (RateLimitedException) - { - Log.Warning("You've been ratelimited. Wait 2 hours to change your name"); - } - - await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName))); - } - - [Cmd] - [UserPerm(GuildPerm.ManageNicknames)] - [BotPerm(GuildPerm.ChangeNickname)] - [Priority(0)] - public async Task SetNick([Leftover] string newNick = null) - { - if (string.IsNullOrWhiteSpace(newNick)) - return; - var curUser = await ctx.Guild.GetCurrentUserAsync(); - await curUser.ModifyAsync(u => u.Nickname = newNick); - - await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-")); - } - - [Cmd] - [BotPerm(GuildPerm.ManageNicknames)] - [UserPerm(GuildPerm.ManageNicknames)] - [Priority(1)] - public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null) - { - var sg = (SocketGuild)ctx.Guild; - if (sg.OwnerId == gu.Id - || gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i); - return; - } - - await gu.ModifyAsync(u => u.Nickname = newNick); - - await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-")); - } - - [Cmd] - [OwnerOnly] - public async Task SetStatus([Leftover] SettableUserStatus status) - { - await _client.SetStatusAsync(SettableUserStatusToUserStatus(status)); - - await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString()))); - } - - [Cmd] - [OwnerOnly] - public async Task SetAvatar([Leftover] string img = null) - { - var success = await _service.SetAvatar(img); - - if (success) - await ReplyConfirmLocalizedAsync(strs.set_avatar); - } - - [Cmd] - [OwnerOnly] - public async Task SetGame(ActivityType type, [Leftover] string game = null) - { - var rep = new ReplacementBuilder().WithDefault(Context).Build(); - - await _service.SetGameAsync(game is null ? game : rep.Replace(game), type); - - await ReplyConfirmLocalizedAsync(strs.set_game); - } - - [Cmd] - [OwnerOnly] - public async Task SetStream(string url, [Leftover] string name = null) - { - name ??= ""; - - await _service.SetStreamAsync(name, url); - - await ReplyConfirmLocalizedAsync(strs.set_stream); - } - - [Cmd] - [OwnerOnly] - public async Task Send(string where, [Leftover] SmartText text = null) - { - var ids = where.Split('|'); - if (ids.Length != 2) - return; - - var sid = ulong.Parse(ids[0]); - var server = _client.Guilds.FirstOrDefault(s => s.Id == sid); - - if (server is null) - return; - - var rep = new ReplacementBuilder().WithDefault(Context).Build(); - - if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture)) - { - var cid = ulong.Parse(ids[1][2..]); - var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid); - if (ch is null) - return; - - text = rep.Replace(text); - await ch.SendAsync(text); - } - else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture)) - { - var uid = ulong.Parse(ids[1][2..]); - var user = server.Users.FirstOrDefault(u => u.Id == uid); - if (user is null) - return; - - var ch = await user.CreateDMChannelAsync(); - text = rep.Replace(text); - await ch.SendAsync(text); - } - else - { - await ReplyErrorLocalizedAsync(strs.invalid_format); - return; - } - - await ReplyConfirmLocalizedAsync(strs.message_sent); - } - - [Cmd] - [OwnerOnly] - public async Task StringsReload() - { - _strings.Reload(); - await _marmaladeLoader.ReloadStrings(); - await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded); - } - - [Cmd] - [OwnerOnly] - public async Task CoordReload() - { - await _coord.Reload(); - await ctx.OkAsync(); - } - - private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus) - { - switch (sus) - { - case SettableUserStatus.Online: - return UserStatus.Online; - case SettableUserStatus.Invisible: - return UserStatus.Invisible; - case SettableUserStatus.Idle: - return UserStatus.AFK; - case SettableUserStatus.Dnd: - return UserStatus.DoNotDisturb; - } - - return UserStatus.Online; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Self/SelfService.cs b/src/Ellie.Bot.Modules.Administration/Self/SelfService.cs deleted file mode 100644 index a0006c6..0000000 --- a/src/Ellie.Bot.Modules.Administration/Self/SelfService.cs +++ /dev/null @@ -1,399 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Services.Database.Models; -using System.Collections.Immutable; - -namespace Ellie.Modules.Administration.Services; - -public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService -{ - private readonly CommandHandler _cmdHandler; - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly DiscordSocketClient _client; - - private readonly IBotCredentials _creds; - - private ImmutableDictionary ownerChannels = - new Dictionary().ToImmutableDictionary(); - - private ConcurrentDictionary> autoCommands = new(); - - private readonly IHttpClientFactory _httpFactory; - private readonly BotConfigService _bss; - private readonly IPubSub _pubSub; - private readonly IEmbedBuilderService _eb; - - //keys - private readonly TypedKey _activitySetKey; - private readonly TypedKey _guildLeaveKey; - - public SelfService( - DiscordSocketClient client, - CommandHandler cmdHandler, - DbService db, - IBotStrings strings, - IBotCredentials creds, - IHttpClientFactory factory, - BotConfigService bss, - IPubSub pubSub, - IEmbedBuilderService eb) - { - _cmdHandler = cmdHandler; - _db = db; - _strings = strings; - _client = client; - _creds = creds; - _httpFactory = factory; - _bss = bss; - _pubSub = pubSub; - _eb = eb; - _activitySetKey = new("activity.set"); - _guildLeaveKey = new("guild.leave"); - - HandleStatusChanges(); - - _pubSub.Sub(_guildLeaveKey, - async input => - { - var guildStr = input.ToString().Trim().ToUpperInvariant(); - if (string.IsNullOrWhiteSpace(guildStr)) - return; - - var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr - || g.Name.Trim().ToUpperInvariant() == guildStr); - if (server is null) - return; - - if (server.OwnerId != _client.CurrentUser.Id) - { - await server.LeaveAsync(); - Log.Information("Left server {Name} [{Id}]", server.Name, server.Id); - } - else - { - await server.DeleteAsync(); - Log.Information("Deleted server {Name} [{Id}]", server.Name, server.Id); - } - }); - } - - public async Task OnReadyAsync() - { - await using var uow = _db.GetDbContext(); - - autoCommands = uow.Set().AsNoTracking() - .Where(x => x.Interval >= 5) - .AsEnumerable() - .GroupBy(x => x.GuildId) - .ToDictionary(x => x.Key, - y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent()) - .ToConcurrent(); - - var startupCommands = uow.Set().AsNoTracking().Where(x => x.Interval == 0); - foreach (var cmd in startupCommands) - { - try - { - await ExecuteCommand(cmd); - } - catch - { - } - } - - if (_client.ShardId == 0) - await LoadOwnerChannels(); - } - - private Timer TimerFromAutoCommand(AutoCommand x) - => new(async obj => await ExecuteCommand((AutoCommand)obj), x, x.Interval * 1000, x.Interval * 1000); - - private async Task ExecuteCommand(AutoCommand cmd) - { - try - { - if (cmd.GuildId is null) - return; - - var guildShard = (int)((cmd.GuildId.Value >> 22) % (ulong)_creds.TotalShards); - if (guildShard != _client.ShardId) - return; - var prefix = _cmdHandler.GetPrefix(cmd.GuildId); - //if someone already has .die as their startup command, ignore it - if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture)) - return; - await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in SelfService ExecuteCommand"); - } - } - - public void AddNewAutoCommand(AutoCommand cmd) - { - using (var uow = _db.GetDbContext()) - { - uow.Set().Add(cmd); - uow.SaveChanges(); - } - - if (cmd.Interval >= 5) - { - var autos = autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary()); - autos.AddOrUpdate(cmd.Id, - _ => TimerFromAutoCommand(cmd), - (_, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return TimerFromAutoCommand(cmd); - }); - } - } - - public IEnumerable GetStartupCommands() - { - using var uow = _db.GetDbContext(); - return uow.Set().AsNoTracking().Where(x => x.Interval == 0).OrderBy(x => x.Id).ToList(); - } - - public IEnumerable GetAutoCommands() - { - using var uow = _db.GetDbContext(); - return uow.Set().AsNoTracking().Where(x => x.Interval >= 5).OrderBy(x => x.Id).ToList(); - } - - private async Task LoadOwnerChannels() - { - var channels = await _creds.OwnerIds.Select(id => - { - var user = _client.GetUser(id); - if (user is null) - return Task.FromResult(null); - - return user.CreateDMChannelAsync(); - }) - .WhenAll(); - - ownerChannels = channels.Where(x => x is not null) - .ToDictionary(x => x.Recipient.Id, x => x) - .ToImmutableDictionary(); - - if (!ownerChannels.Any()) - { - Log.Warning( - "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server"); - } - else - { - Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels", - ownerChannels.Count, - _creds.OwnerIds.Count); - } - } - - public Task LeaveGuild(string guildStr) - => _pubSub.Pub(_guildLeaveKey, guildStr); - - // forwards dms - public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - { - var bs = _bss.Data; - if (msg.Channel is IDMChannel && bs.ForwardMessages && (ownerChannels.Any() || bs.ForwardToChannel is not null)) - { - var title = _strings.GetText(strs.dm_from) + $" [{msg.Author}]({msg.Author.Id})"; - - var attachamentsTxt = _strings.GetText(strs.attachments); - - var toSend = msg.Content; - - if (msg.Attachments.Count > 0) - { - toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n" - + string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl)); - } - - if (bs.ForwardToAllOwners) - { - var allOwnerChannels = ownerChannels.Values; - - foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id)) - { - try - { - await ownerCh.SendConfirmAsync(_eb, title, toSend); - } - catch - { - Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id); - } - } - } - else if (bs.ForwardToChannel is ulong cid) - { - try - { - if (_client.GetChannel(cid) is ITextChannel ch) - await ch.SendConfirmAsync(_eb, title, toSend); - } - catch - { - Log.Warning("Error forwarding message to the channel"); - } - } - else - { - var firstOwnerChannel = ownerChannels.Values.First(); - if (firstOwnerChannel.Recipient.Id != msg.Author.Id) - { - try - { - await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend); - } - catch - { - // ignored - } - } - } - } - } - - public bool RemoveStartupCommand(int index, out AutoCommand cmd) - { - using var uow = _db.GetDbContext(); - cmd = uow.Set().AsNoTracking().Where(x => x.Interval == 0).Skip(index).FirstOrDefault(); - - if (cmd is not null) - { - uow.Remove(cmd); - uow.SaveChanges(); - return true; - } - - return false; - } - - public bool RemoveAutoCommand(int index, out AutoCommand cmd) - { - using var uow = _db.GetDbContext(); - cmd = uow.Set().AsNoTracking().Where(x => x.Interval >= 5).Skip(index).FirstOrDefault(); - - if (cmd is not null) - { - uow.Remove(cmd); - if (autoCommands.TryGetValue(cmd.GuildId, out var autos)) - { - if (autos.TryRemove(cmd.Id, out var timer)) - timer.Change(Timeout.Infinite, Timeout.Infinite); - } - - uow.SaveChanges(); - return true; - } - - return false; - } - - public async Task SetAvatar(string img) - { - if (string.IsNullOrWhiteSpace(img)) - return false; - - if (!Uri.IsWellFormedUriString(img, UriKind.Absolute)) - return false; - - var uri = new Uri(img); - - using var http = _httpFactory.CreateClient(); - using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); - if (!sr.IsImage()) - return false; - - // i can't just do ReadAsStreamAsync because dicord.net's image poops itself - var imgData = await sr.Content.ReadAsByteArrayAsync(); - await using var imgStream = imgData.ToStream(); - await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)); - - return true; - } - - public void ClearStartupCommands() - { - using var uow = _db.GetDbContext(); - var toRemove = uow.Set().AsNoTracking().Where(x => x.Interval == 0); - - uow.Set().RemoveRange(toRemove); - uow.SaveChanges(); - } - - public bool ForwardMessages() - { - var isForwarding = false; - _bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; }); - - return isForwarding; - } - - public bool ForwardToAll() - { - var isToAll = false; - _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; }); - return isToAll; - } - - public bool ForwardToChannel(ulong? channelId) - { - using var uow = _db.GetDbContext(); - - _bss.ModifyConfig(config => - { - config.ForwardToChannel = channelId == config.ForwardToChannel - ? null - : channelId; - }); - - return channelId is not null; - } - - private void HandleStatusChanges() - => _pubSub.Sub(_activitySetKey, - async data => - { - try - { - await _client.SetGameAsync(data.Name, data.Link, data.Type); - } - catch (Exception ex) - { - Log.Warning(ex, "Error setting activity"); - } - }); - - public Task SetGameAsync(string game, ActivityType type) - => _pubSub.Pub(_activitySetKey, - new() - { - Name = game, - Link = null, - Type = type - }); - - public Task SetStreamAsync(string name, string link) - => _pubSub.Pub(_activitySetKey, - new() - { - Name = name, - Link = link, - Type = ActivityType.Streaming - }); - - private sealed class ActivityPubData - { - public string Name { get; init; } - public string Link { get; init; } - public ActivityType Type { get; init; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesCommands.cs b/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesCommands.cs deleted file mode 100644 index 6a0fc2a..0000000 --- a/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesCommands.cs +++ /dev/null @@ -1,234 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; -using System.Text; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class SelfAssignedRolesCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.ManageMessages)] - public async Task AdSarm() - { - var newVal = _service.ToggleAdSarm(ctx.Guild.Id); - - if (newVal) - await ReplyConfirmLocalizedAsync(strs.adsarm_enable(prefix)); - else - await ReplyConfirmLocalizedAsync(strs.adsarm_disable(prefix)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public Task Asar([Leftover] IRole role) - => Asar(0, role); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task Asar(int group, [Leftover] IRole role) - { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - var succ = _service.AddNew(ctx.Guild.Id, role, group); - - if (succ) - { - await ReplyConfirmLocalizedAsync(strs.role_added(Format.Bold(role.Name), - Format.Bold(@group.ToString()))); - } - else - await ReplyErrorLocalizedAsync(strs.role_in_list(Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task Sargn(int group, [Leftover] string name = null) - { - var set = await _service.SetNameAsync(ctx.Guild.Id, group, name); - - if (set) - { - await ReplyConfirmLocalizedAsync( - strs.group_name_added(Format.Bold(@group.ToString()), Format.Bold(name))); - } - else - await ReplyConfirmLocalizedAsync(strs.group_name_removed(Format.Bold(group.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task Rsar([Leftover] IRole role) - { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - var success = _service.RemoveSar(role.Guild.Id, role.Id); - if (!success) - await ReplyErrorLocalizedAsync(strs.self_assign_not); - else - await ReplyConfirmLocalizedAsync(strs.self_assign_rem(Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Lsar(int page = 1) - { - if (--page < 0) - return; - - var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild); - - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var rolesStr = new StringBuilder(); - var roleGroups = roles.OrderBy(x => x.Model.Group) - .Skip(cur * 20) - .Take(20) - .GroupBy(x => x.Model.Group) - .OrderBy(x => x.Key); - - foreach (var kvp in roleGroups) - { - string groupNameText; - if (!groups.TryGetValue(kvp.Key, out var name)) - groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key))); - else - groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}"); - - rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫"); - foreach (var (model, role) in kvp.AsEnumerable()) - { - if (role is null) - { - } - else - { - // first character is invisible space - if (model.LevelRequirement == 0) - rolesStr.AppendLine("‌‌ " + role.Name); - else - rolesStr.AppendLine("‌‌ " + role.Name + $" (lvl {model.LevelRequirement}+)"); - } - } - - rolesStr.AppendLine(); - } - - return _eb.Create() - .WithOkColor() - .WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count())))) - .WithDescription(rolesStr.ToString()) - .WithFooter(exclusive - ? GetText(strs.self_assign_are_exclusive) - : GetText(strs.self_assign_are_not_exclusive)); - }, - roles.Count(), - 20); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task Togglexclsar() - { - var areExclusive = _service.ToggleEsar(ctx.Guild.Id); - if (areExclusive) - await ReplyConfirmLocalizedAsync(strs.self_assign_excl); - else - await ReplyConfirmLocalizedAsync(strs.self_assign_no_excl); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RoleLevelReq(int level, [Leftover] IRole role) - { - if (level < 0) - return; - - var succ = _service.SetLevelReq(ctx.Guild.Id, role, level); - - if (!succ) - { - await ReplyErrorLocalizedAsync(strs.self_assign_not); - return; - } - - await ReplyConfirmLocalizedAsync(strs.self_assign_level_req(Format.Bold(role.Name), - Format.Bold(level.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Iam([Leftover] IRole role) - { - var guildUser = (IGuildUser)ctx.User; - - var (result, autoDelete, extra) = await _service.Assign(guildUser, role); - - IUserMessage msg; - if (result == SelfAssignedRolesService.AssignResult.ErrNotAssignable) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not); - else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_level(Format.Bold(extra.ToString()))); - else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_already(Format.Bold(role.Name))); - else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms); - else - msg = await ReplyConfirmLocalizedAsync(strs.self_assign_success(Format.Bold(role.Name))); - - if (autoDelete) - { - msg.DeleteAfter(3); - ctx.Message.DeleteAfter(3); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Iamnot([Leftover] IRole role) - { - var guildUser = (IGuildUser)ctx.User; - - var (result, autoDelete) = await _service.Remove(guildUser, role); - - IUserMessage msg; - if (result == SelfAssignedRolesService.RemoveResult.ErrNotAssignable) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not); - else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_have(Format.Bold(role.Name))); - else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms) - msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms); - else - msg = await ReplyConfirmLocalizedAsync(strs.self_assign_remove(Format.Bold(role.Name))); - - if (autoDelete) - { - msg.DeleteAfter(3); - ctx.Message.DeleteAfter(3); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesService.cs b/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesService.cs deleted file mode 100644 index 4939659..0000000 --- a/src/Ellie.Bot.Modules.Administration/SelfAssignableRoles/SelfAssignedRolesService.cs +++ /dev/null @@ -1,234 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public class SelfAssignedRolesService : IEService -{ - public enum AssignResult - { - Assigned, // successfully removed - ErrNotAssignable, // not assignable (error) - ErrAlreadyHave, // you already have that role (error) - ErrNotPerms, // bot doesn't have perms (error) - ErrLvlReq // you are not required level (error) - } - - public enum RemoveResult - { - Removed, // successfully removed - ErrNotAssignable, // not assignable (error) - ErrNotHave, // you don't have a role you want to remove (error) - ErrNotPerms // bot doesn't have perms (error) - } - - private readonly DbService _db; - - public SelfAssignedRolesService(DbService db) - => _db = db; - - public bool AddNew(ulong guildId, IRole role, int group) - { - using var uow = _db.GetDbContext(); - var roles = uow.Set().GetFromGuild(guildId); - if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) - return false; - - uow.Set().Add(new() - { - Group = group, - RoleId = role.Id, - GuildId = role.Guild.Id - }); - uow.SaveChanges(); - return true; - } - - public bool ToggleAdSarm(ulong guildId) - { - bool newval; - using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, set => set); - newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages; - uow.SaveChanges(); - return newval; - } - - public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role) - { - LevelStats userLevelData; - await using (var uow = _db.GetDbContext()) - { - var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id); - userLevelData = new(stats.Xp + stats.AwardedXp); - } - - var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id); - - var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id); - if (theRoleYouWant is null) - return (AssignResult.ErrNotAssignable, autoDelete, null); - if (theRoleYouWant.LevelRequirement > userLevelData.Level) - return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement); - if (guildUser.RoleIds.Contains(role.Id)) - return (AssignResult.ErrAlreadyHave, autoDelete, null); - - var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray(); - if (exclusive) - { - var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r)); - - foreach (var roleId in sameRoles) - { - var sameRole = guildUser.Guild.GetRole(roleId); - if (sameRole is not null) - { - try - { - await guildUser.RemoveRoleAsync(sameRole); - await Task.Delay(300); - } - catch - { - // ignored - } - } - } - } - - try - { - await guildUser.AddRoleAsync(role); - } - catch (Exception ex) - { - return (AssignResult.ErrNotPerms, autoDelete, ex); - } - - return (AssignResult.Assigned, autoDelete, null); - } - - public async Task SetNameAsync(ulong guildId, int group, string name) - { - var set = false; - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames)); - var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group); - - if (string.IsNullOrWhiteSpace(name)) - { - if (toUpdate is not null) - gc.SelfAssignableRoleGroupNames.Remove(toUpdate); - } - else if (toUpdate is null) - { - gc.SelfAssignableRoleGroupNames.Add(new() - { - Name = name, - Number = group - }); - set = true; - } - else - { - toUpdate.Name = name; - set = true; - } - - await uow.SaveChangesAsync(); - - return set; - } - - public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role) - { - var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id); - - if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null) - return (RemoveResult.ErrNotAssignable, autoDelete); - if (!guildUser.RoleIds.Contains(role.Id)) - return (RemoveResult.ErrNotHave, autoDelete); - try - { - await guildUser.RemoveRoleAsync(role); - } - catch (Exception) - { - return (RemoveResult.ErrNotPerms, autoDelete); - } - - return (RemoveResult.Removed, autoDelete); - } - - public bool RemoveSar(ulong guildId, ulong roleId) - { - bool success; - using var uow = _db.GetDbContext(); - success = uow.Set().DeleteByGuildAndRoleId(guildId, roleId); - uow.SaveChanges(); - return success; - } - - public (bool AutoDelete, bool Exclusive, IReadOnlyCollection) GetAdAndRoles(ulong guildId) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages; - var exclusive = gc.ExclusiveSelfAssignedRoles; - var roles = uow.Set().GetFromGuild(guildId); - - return (autoDelete, exclusive, roles); - } - - public bool SetLevelReq(ulong guildId, IRole role, int level) - { - using var uow = _db.GetDbContext(); - var roles = uow.Set().GetFromGuild(guildId); - var sar = roles.FirstOrDefault(x => x.RoleId == role.Id); - if (sar is not null) - { - sar.LevelRequirement = level; - uow.SaveChanges(); - } - else - return false; - - return true; - } - - public bool ToggleEsar(ulong guildId) - { - bool areExclusive; - using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, set => set); - - areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles; - uow.SaveChanges(); - return areExclusive; - } - - public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary - GroupNames - ) GetRoles(IGuild guild) - { - var exclusive = false; - - IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> roles; - IDictionary groupNames; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames)); - exclusive = gc.ExclusiveSelfAssignedRoles; - groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name); - var roleModels = uow.Set().GetFromGuild(guild.Id); - roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId))) - .ToList(); - uow.Set().RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray()); - uow.SaveChanges(); - } - - return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/ServerLog/DummyLogCommandService.cs b/src/Ellie.Bot.Modules.Administration/ServerLog/DummyLogCommandService.cs deleted file mode 100644 index 22be6b2..0000000 --- a/src/Ellie.Bot.Modules.Administration/ServerLog/DummyLogCommandService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public sealed class DummyLogCommandService : ILogCommandService -#if GLOBAL_ELLIE -, IEService -#endif -{ - public void AddDeleteIgnore(ulong xId) - { - } - - public Task LogServer(ulong guildId, ulong channelId, bool actionValue) - => Task.CompletedTask; - - public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType) - => false; - - public LogSetting? GetGuildLogSettings(ulong guildId) - => default; - - public bool Log(ulong guildId, ulong? channelId, LogType type) - => false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommandService.cs b/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommandService.cs deleted file mode 100644 index 770d688..0000000 --- a/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommandService.cs +++ /dev/null @@ -1,1358 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Modules.Administration.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public sealed class LogCommandService : ILogCommandService, IReadyExecutor -#if !GLOBAL_ELLIE - , IEService // don't load this service on global ellie -#endif -{ - public ConcurrentDictionary GuildLogSettings { get; } - - private ConcurrentDictionary> PresenceUpdates { get; } = new(); - private readonly DiscordSocketClient _client; - - private readonly IBotStrings _strings; - private readonly DbService _db; - private readonly MuteService _mute; - private readonly ProtectionService _prot; - private readonly GuildTimezoneService _tz; - private readonly IEmbedBuilderService _eb; - private readonly IMemoryCache _memoryCache; - - private readonly ConcurrentHashSet _ignoreMessageIds = new(); - private readonly UserPunishService _punishService; - - public LogCommandService( - DiscordSocketClient client, - IBotStrings strings, - DbService db, - MuteService mute, - ProtectionService prot, - GuildTimezoneService tz, - IMemoryCache memoryCache, - IEmbedBuilderService eb, - UserPunishService punishService) - { - _client = client; - _memoryCache = memoryCache; - _eb = eb; - _strings = strings; - _db = db; - _mute = mute; - _prot = prot; - _tz = tz; - _punishService = punishService; - - using (var uow = db.GetDbContext()) - { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set().AsQueryable() - .AsNoTracking() - .Where(x => guildIds.Contains(x.GuildId)) - .Include(ls => ls.LogIgnores) - .ToList(); - - GuildLogSettings = configs.ToDictionary(ls => ls.GuildId).ToConcurrent(); - } - - //_client.MessageReceived += _client_MessageReceived; - _client.MessageUpdated += _client_MessageUpdated; - _client.MessageDeleted += _client_MessageDeleted; - _client.UserBanned += _client_UserBanned; - _client.UserUnbanned += _client_UserUnbanned; - _client.UserJoined += _client_UserJoined; - _client.UserLeft += _client_UserLeft; - // _client.PresenceUpdated += _client_UserPresenceUpdated; - _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; - _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; - _client.GuildMemberUpdated += _client_GuildUserUpdated; - _client.PresenceUpdated += _client_PresenceUpdated; - _client.UserUpdated += _client_UserUpdated; - _client.ChannelCreated += _client_ChannelCreated; - _client.ChannelDestroyed += _client_ChannelDestroyed; - _client.ChannelUpdated += _client_ChannelUpdated; - _client.RoleDeleted += _client_RoleDeleted; - - _client.ThreadCreated += _client_ThreadCreated; - _client.ThreadDeleted += _client_ThreadDeleted; - - _mute.UserMuted += MuteCommands_UserMuted; - _mute.UserUnmuted += MuteCommands_UserUnmuted; - - _prot.OnAntiProtectionTriggered += TriggeredAntiProtection; - - _punishService.OnUserWarned += PunishServiceOnOnUserWarned; - } - - private async Task _client_PresenceUpdated(SocketUser user, SocketPresence? before, SocketPresence? after) - { - if (user is not SocketGuildUser gu) - return; - - if (!GuildLogSettings.TryGetValue(gu.Guild.Id, out var logSetting) - || before is null - || after is null) - return; - - ITextChannel? logChannel; - - if (!user.IsBot - && logSetting.LogUserPresenceId is not null - && (logChannel = - await TryGetLogChannel(gu.Guild, logSetting, LogType.UserPresence)) is not null) - { - if (before.Status != after.Status) - { - var str = "🎭" - + Format.Code(PrettyCurrentTime(gu.Guild)) - + GetText(logChannel.Guild, - strs.user_status_change("👤" + Format.Bold(gu.Username), - Format.Bold(after.Status.ToString()))); - PresenceUpdates.AddOrUpdate(logChannel, - new List - { - str - }, - (_, list) => - { - list.Add(str); - return list; - }); - } - else if (before.Activities.FirstOrDefault()?.Name != after.Activities.FirstOrDefault()?.Name) - { - var str = - $"👾`{PrettyCurrentTime(gu.Guild)}`👤__**{gu.Username}**__ is now playing **{after.Activities.FirstOrDefault()?.Name ?? "-"}**."; - PresenceUpdates.AddOrUpdate(logChannel, - new List - { - str - }, - (_, list) => - { - list.Add(str); - return list; - }); - } - } - } - - private Task _client_ThreadDeleted(Cacheable sch) - { - _ = Task.Run(async () => - { - try - { - if (!sch.HasValue) - return; - - var ch = sch.Value; - - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out var logSetting) - || logSetting.ThreadDeletedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ThreadDeleted)) is null) - return; - - var title = GetText(logChannel.Guild, strs.thread_deleted); - - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🗑 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ThreadCreated(SocketThreadChannel ch) - { - _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out var logSetting) - || logSetting.ThreadCreatedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ThreadCreated)) is null) - return; - - var title = GetText(logChannel.Guild, strs.thread_created); - - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🆕 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))); - } - catch (Exception) - { - // ignored - } - }); - - return Task.CompletedTask; - } - - public async Task OnReadyAsync() - => await Task.WhenAll(PresenceUpdateTask(), IgnoreMessageIdsClearTask()); - - private async Task IgnoreMessageIdsClearTask() - { - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - while (await timer.WaitForNextTickAsync()) - _ignoreMessageIds.Clear(); - } - - private async Task PresenceUpdateTask() - { - using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); - while (await timer.WaitForNextTickAsync()) - { - try - { - var keys = PresenceUpdates.Keys.ToList(); - - await keys.Select(key => - { - if (!((SocketGuild)key.Guild).CurrentUser.GetPermissions(key).SendMessages) - return Task.CompletedTask; - - if (PresenceUpdates.TryRemove(key, out var msgs)) - { - var title = GetText(key.Guild, strs.presence_updates); - var desc = string.Join(Environment.NewLine, msgs); - return key.SendConfirmAsync(_eb, title, desc.TrimTo(2048)!); - } - - return Task.CompletedTask; - }) - .WhenAll(); - } - catch - { - } - } - } - - public LogSetting? GetGuildLogSettings(ulong guildId) - { - GuildLogSettings.TryGetValue(guildId, out var logSetting); - return logSetting; - } - - public void AddDeleteIgnore(ulong messageId) - => _ignoreMessageIds.Add(messageId); - - public bool LogIgnore(ulong gid, ulong itemId, IgnoredItemType itemType) - { - using var uow = _db.GetDbContext(); - var logSetting = uow.LogSettingsFor(gid); - var removed = logSetting.LogIgnores.RemoveAll(x => x.ItemType == itemType && itemId == x.LogItemId); - - if (removed == 0) - { - var toAdd = new IgnoredLogItem - { - LogItemId = itemId, - ItemType = itemType - }; - logSetting.LogIgnores.Add(toAdd); - } - - uow.SaveChanges(); - GuildLogSettings.AddOrUpdate(gid, logSetting, (_, _) => logSetting); - return removed > 0; - } - - private string GetText(IGuild guild, LocStr str) - => _strings.GetText(str, guild.Id); - - private string PrettyCurrentTime(IGuild? g) - { - var time = DateTime.UtcNow; - if (g is not null) - time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); - return $"【{time:HH:mm:ss}】"; - } - - private string CurrentTime(IGuild? g) - { - var time = DateTime.UtcNow; - if (g is not null) - time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); - - return $"{time:HH:mm:ss}"; - } - - public async Task LogServer(ulong guildId, ulong channelId, bool value) - { - await using var uow = _db.GetDbContext(); - var logSetting = uow.LogSettingsFor(guildId); - - logSetting.LogOtherId = logSetting.MessageUpdatedId = logSetting.MessageDeletedId = logSetting.UserJoinedId = - logSetting.UserLeftId = logSetting.UserBannedId = logSetting.UserUnbannedId = logSetting.UserUpdatedId = - logSetting.ChannelCreatedId = logSetting.ChannelDestroyedId = logSetting.ChannelUpdatedId = - logSetting.LogUserPresenceId = logSetting.LogVoicePresenceId = logSetting.UserMutedId = - logSetting.LogVoicePresenceTTSId = logSetting.ThreadCreatedId = logSetting.ThreadDeletedId - = logSetting.LogWarnsId = value ? channelId : null; - await uow.SaveChangesAsync(); - GuildLogSettings.AddOrUpdate(guildId, _ => logSetting, (_, _) => logSetting); - } - - - private async Task PunishServiceOnOnUserWarned(Warning arg) - { - if (!GuildLogSettings.TryGetValue(arg.GuildId, out var logSetting) || logSetting.LogWarnsId is null) - return; - - var g = _client.GetGuild(arg.GuildId); - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserWarned)) is null) - return; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle($"⚠️ User Warned") - .WithDescription($"<@{arg.UserId}> | {arg.UserId}") - .AddField("Mod", arg.Moderator) - .AddField("Reason", string.IsNullOrWhiteSpace(arg.Reason) ? "-" : arg.Reason, true) - .WithFooter(CurrentTime(g)); - - await logChannel.EmbedAsync(embed); - } - - private Task _client_UserUpdated(SocketUser before, SocketUser uAfter) - { - _ = Task.Run(async () => - { - try - { - if (uAfter is not SocketGuildUser after) - return; - - var g = after.Guild; - - if (!GuildLogSettings.TryGetValue(g.Id, out var logSetting) || logSetting.UserUpdatedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserUpdated)) is null) - return; - - var embed = _eb.Create(); - - if (before.Username != after.Username) - { - embed.WithTitle("👥 " + GetText(g, strs.username_changed)) - .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") - .AddField("Old Name", $"{before.Username}", true) - .AddField("New Name", $"{after.Username}", true) - .WithFooter(CurrentTime(g)) - .WithOkColor(); - } - else if (before.AvatarId != after.AvatarId) - { - embed.WithTitle("👥" + GetText(g, strs.avatar_changed)) - .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") - .WithFooter(CurrentTime(g)) - .WithOkColor(); - - var bav = before.RealAvatarUrl(); - if (bav.IsAbsoluteUri) - embed.WithThumbnailUrl(bav.ToString()); - - var aav = after.RealAvatarUrl(); - if (aav.IsAbsoluteUri) - embed.WithImageUrl(aav.ToString()); - } - else - return; - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - public bool Log(ulong gid, ulong? cid, LogType type /*, string options*/) - { - ulong? channelId = null; - using (var uow = _db.GetDbContext()) - { - var logSetting = uow.LogSettingsFor(gid); - GuildLogSettings.AddOrUpdate(gid, _ => logSetting, (_, _) => logSetting); - switch (type) - { - case LogType.Other: - channelId = logSetting.LogOtherId = logSetting.LogOtherId is null ? cid : default; - break; - case LogType.MessageUpdated: - channelId = logSetting.MessageUpdatedId = logSetting.MessageUpdatedId is null ? cid : default; - break; - case LogType.MessageDeleted: - channelId = logSetting.MessageDeletedId = logSetting.MessageDeletedId is null ? cid : default; - //logSetting.DontLogBotMessageDeleted = (options == "nobot"); - break; - case LogType.UserJoined: - channelId = logSetting.UserJoinedId = logSetting.UserJoinedId is null ? cid : default; - break; - case LogType.UserLeft: - channelId = logSetting.UserLeftId = logSetting.UserLeftId is null ? cid : default; - break; - case LogType.UserBanned: - channelId = logSetting.UserBannedId = logSetting.UserBannedId is null ? cid : default; - break; - case LogType.UserUnbanned: - channelId = logSetting.UserUnbannedId = logSetting.UserUnbannedId is null ? cid : default; - break; - case LogType.UserUpdated: - channelId = logSetting.UserUpdatedId = logSetting.UserUpdatedId is null ? cid : default; - break; - case LogType.UserMuted: - channelId = logSetting.UserMutedId = logSetting.UserMutedId is null ? cid : default; - break; - case LogType.ChannelCreated: - channelId = logSetting.ChannelCreatedId = logSetting.ChannelCreatedId is null ? cid : default; - break; - case LogType.ChannelDestroyed: - channelId = logSetting.ChannelDestroyedId = logSetting.ChannelDestroyedId is null ? cid : default; - break; - case LogType.ChannelUpdated: - channelId = logSetting.ChannelUpdatedId = logSetting.ChannelUpdatedId is null ? cid : default; - break; - case LogType.UserPresence: - channelId = logSetting.LogUserPresenceId = logSetting.LogUserPresenceId is null ? cid : default; - break; - case LogType.VoicePresence: - channelId = logSetting.LogVoicePresenceId = logSetting.LogVoicePresenceId is null ? cid : default; - break; - case LogType.VoicePresenceTts: - channelId = logSetting.LogVoicePresenceTTSId = - logSetting.LogVoicePresenceTTSId is null ? cid : default; - break; - case LogType.UserWarned: - channelId = logSetting.LogWarnsId = logSetting.LogWarnsId is null ? cid : default; - break; - case LogType.ThreadDeleted: - channelId = logSetting.ThreadDeletedId = logSetting.ThreadDeletedId is null ? cid : default; - break; - case LogType.ThreadCreated: - channelId = logSetting.ThreadCreatedId = logSetting.ThreadCreatedId is null ? cid : default; - break; - } - - uow.SaveChanges(); - } - - return channelId is not null; - } - - private Task _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) - { - _ = Task.Run(async () => - { - try - { - if (iusr is not IGuildUser usr) - return; - - var beforeVch = before.VoiceChannel; - var afterVch = after.VoiceChannel; - - if (beforeVch == afterVch) - return; - - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out var logSetting) - || logSetting.LogVoicePresenceTTSId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresenceTts)) is null) - return; - - var str = string.Empty; - if (beforeVch?.Guild == afterVch?.Guild) - str = GetText(logChannel.Guild, strs.log_vc_moved(usr.Username, beforeVch?.Name, afterVch?.Name)); - else if (beforeVch is null) - str = GetText(logChannel.Guild, strs.log_vc_joined(usr.Username, afterVch?.Name)); - else if (afterVch is null) - str = GetText(logChannel.Guild, strs.log_vc_left(usr.Username, beforeVch.Name)); - - var toDelete = await logChannel.SendMessageAsync(str, true); - toDelete.DeleteAfter(5); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private void MuteCommands_UserMuted( - IGuildUser usr, - IUser mod, - MuteType muteType, - string reason) - => _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out var logSetting) || logSetting.UserMutedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted)) is null) - return; - var mutes = string.Empty; - var mutedLocalized = GetText(logChannel.Guild, strs.muted_sn); - switch (muteType) - { - case MuteType.Voice: - mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_voice(mutedLocalized, mod.ToString())); - break; - case MuteType.Chat: - mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_text(mutedLocalized, mod.ToString())); - break; - case MuteType.All: - mutes = "🔇 " - + GetText(logChannel.Guild, strs.xmuted_text_and_voice(mutedLocalized, mod.ToString())); - break; - } - - var embed = _eb.Create() - .WithAuthor(mutes) - .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter(CurrentTime(usr.Guild)) - .WithOkColor(); - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - - private void MuteCommands_UserUnmuted( - IGuildUser usr, - IUser mod, - MuteType muteType, - string reason) - => _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out var logSetting) || logSetting.UserMutedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted)) is null) - return; - - var mutes = string.Empty; - var unmutedLocalized = GetText(logChannel.Guild, strs.unmuted_sn); - switch (muteType) - { - case MuteType.Voice: - mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_voice(unmutedLocalized, mod.ToString())); - break; - case MuteType.Chat: - mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_text(unmutedLocalized, mod.ToString())); - break; - case MuteType.All: - mutes = "🔊 " - + GetText(logChannel.Guild, - strs.xmuted_text_and_voice(unmutedLocalized, mod.ToString())); - break; - } - - var embed = _eb.Create() - .WithAuthor(mutes) - .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter($"{CurrentTime(usr.Guild)}") - .WithOkColor(); - - if (!string.IsNullOrWhiteSpace(reason)) - embed.WithDescription(reason); - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - - public Task TriggeredAntiProtection(PunishmentAction action, ProtectionType protection, params IGuildUser[] users) - { - _ = Task.Run(async () => - { - try - { - if (users.Length == 0) - return; - - if (!GuildLogSettings.TryGetValue(users.First().Guild.Id, out var logSetting) - || logSetting.LogOtherId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(users.First().Guild, logSetting, LogType.Other)) is null) - return; - - var punishment = string.Empty; - switch (action) - { - case PunishmentAction.Mute: - punishment = "🔇 " + GetText(logChannel.Guild, strs.muted_pl).ToUpperInvariant(); - break; - case PunishmentAction.Kick: - punishment = "👢 " + GetText(logChannel.Guild, strs.kicked_pl).ToUpperInvariant(); - break; - case PunishmentAction.Softban: - punishment = "☣ " + GetText(logChannel.Guild, strs.soft_banned_pl).ToUpperInvariant(); - break; - case PunishmentAction.Ban: - punishment = "⛔️ " + GetText(logChannel.Guild, strs.banned_pl).ToUpperInvariant(); - break; - case PunishmentAction.RemoveRoles: - punishment = "⛔️ " + GetText(logChannel.Guild, strs.remove_roles_pl).ToUpperInvariant(); - break; - } - - var embed = _eb.Create() - .WithAuthor($"🛡 Anti-{protection}") - .WithTitle(GetText(logChannel.Guild, strs.users) + " " + punishment) - .WithDescription(string.Join("\n", users.Select(u => u.ToString()))) - .WithFooter(CurrentTime(logChannel.Guild)) - .WithOkColor(); - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private string GetRoleDeletedKey(ulong roleId) - => $"role_deleted_{roleId}"; - - private Task _client_RoleDeleted(SocketRole socketRole) - { - Serilog.Log.Information("Role deleted {RoleId}", socketRole.Id); - _memoryCache.Set(GetRoleDeletedKey(socketRole.Id), true, TimeSpan.FromMinutes(5)); - return Task.CompletedTask; - } - - private bool IsRoleDeleted(ulong roleId) - { - var isDeleted = _memoryCache.TryGetValue(GetRoleDeletedKey(roleId), out _); - return isDeleted; - } - - private Task _client_GuildUserUpdated(Cacheable optBefore, SocketGuildUser after) - { - _ = Task.Run(async () => - { - try - { - var before = await optBefore.GetOrDownloadAsync(); - - if (before is null) - return; - - if (!GuildLogSettings.TryGetValue(before.Guild.Id, out var logSetting) - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel? logChannel; - if (logSetting.UserUpdatedId is not null - && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) is not null) - { - var embed = _eb.Create() - .WithOkColor() - .WithFooter(CurrentTime(before.Guild)) - .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); - if (before.Nickname != after.Nickname) - { - embed.WithAuthor("👥 " + GetText(logChannel.Guild, strs.nick_change)) - .AddField(GetText(logChannel.Guild, strs.old_nick), - $"{before.Nickname}#{before.Discriminator}") - .AddField(GetText(logChannel.Guild, strs.new_nick), - $"{after.Nickname}#{after.Discriminator}"); - - await logChannel.EmbedAsync(embed); - } - else if (!before.Roles.SequenceEqual(after.Roles)) - { - if (before.Roles.Count < after.Roles.Count) - { - var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name); - embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_add)) - .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); - - await logChannel.EmbedAsync(embed); - } - else if (before.Roles.Count > after.Roles.Count) - { - await Task.Delay(1000); - var diffRoles = before.Roles.Where(r => !after.Roles.Contains(r) && !IsRoleDeleted(r.Id)) - .Select(r => r.Name) - .ToList(); - - if (diffRoles.Any()) - { - embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_rem)) - .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); - - await logChannel.EmbedAsync(embed); - } - } - } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ChannelUpdated(IChannel cbefore, IChannel cafter) - { - _ = Task.Run(async () => - { - try - { - if (cbefore is not IGuildChannel before) - return; - - var after = (IGuildChannel)cafter; - - if (!GuildLogSettings.TryGetValue(before.Guild.Id, out var logSetting) - || logSetting.ChannelUpdatedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.ChannelUpdated)) is null) - return; - - var embed = _eb.Create().WithOkColor().WithFooter(CurrentTime(before.Guild)); - - var beforeTextChannel = cbefore as ITextChannel; - var afterTextChannel = cafter as ITextChannel; - - if (before.Name != after.Name) - { - embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_name_change)) - .WithDescription($"{after} | {after.Id}") - .AddField(GetText(logChannel.Guild, strs.ch_old_name), before.Name); - } - else if (beforeTextChannel?.Topic != afterTextChannel?.Topic) - { - embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_topic_change)) - .WithDescription($"{after} | {after.Id}") - .AddField(GetText(logChannel.Guild, strs.old_topic), beforeTextChannel?.Topic ?? "-") - .AddField(GetText(logChannel.Guild, strs.new_topic), afterTextChannel?.Topic ?? "-"); - } - else - return; - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ChannelDestroyed(IChannel ich) - { - _ = Task.Run(async () => - { - try - { - if (ich is not IGuildChannel ch) - return; - - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out var logSetting) - || logSetting.ChannelDestroyedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == ch.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelDestroyed)) is null) - return; - - string title; - if (ch is IVoiceChannel) - title = GetText(logChannel.Guild, strs.voice_chan_destroyed); - else - title = GetText(logChannel.Guild, strs.text_chan_destroyed); - - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🆕 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ChannelCreated(IChannel ich) - { - _ = Task.Run(async () => - { - try - { - if (ich is not IGuildChannel ch) - return; - - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out var logSetting) - || logSetting.ChannelCreatedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelCreated)) is null) - return; - string title; - if (ch is IVoiceChannel) - title = GetText(logChannel.Guild, strs.voice_chan_created); - else - title = GetText(logChannel.Guild, strs.text_chan_created); - - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🆕 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) - { - _ = Task.Run(async () => - { - try - { - if (iusr is not IGuildUser usr || usr.IsBot) - return; - - var beforeVch = before.VoiceChannel; - var afterVch = after.VoiceChannel; - - if (beforeVch == afterVch) - return; - - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out var logSetting) - || logSetting.LogVoicePresenceId is null - || logSetting.LogIgnores.Any( - ilc => ilc.LogItemId == iusr.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresence)) is null) - return; - - var str = string.Empty; - if (beforeVch?.Guild == afterVch?.Guild) - { - str = "🎙" - + Format.Code(PrettyCurrentTime(usr.Guild)) - + GetText(logChannel.Guild, - strs.user_vmoved("👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), - Format.Bold(beforeVch?.Name ?? ""), - Format.Bold(afterVch?.Name ?? ""))); - } - else if (beforeVch is null) - { - str = "🎙" - + Format.Code(PrettyCurrentTime(usr.Guild)) - + GetText(logChannel.Guild, - strs.user_vjoined("👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), - Format.Bold(afterVch?.Name ?? ""))); - } - else if (afterVch is null) - { - str = "🎙" - + Format.Code(PrettyCurrentTime(usr.Guild)) - + GetText(logChannel.Guild, - strs.user_vleft("👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), - Format.Bold(beforeVch.Name ?? ""))); - } - - if (!string.IsNullOrWhiteSpace(str)) - { - PresenceUpdates.AddOrUpdate(logChannel, - new List - { - str - }, - (_, list) => - { - list.Add(str); - return list; - }); - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserLeft(SocketGuild guild, SocketUser usr) - { - _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(guild.Id, out var logSetting) - || logSetting.UserLeftId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserLeft)) is null) - return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("❌ " + GetText(logChannel.Guild, strs.user_left)) - .WithDescription(usr.ToString()) - .AddField("Id", usr.Id.ToString()) - .WithFooter(CurrentTime(guild)); - - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserJoined(IGuildUser usr) - { - _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out var logSetting) || logSetting.UserJoinedId is null) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserJoined)) is null) - return; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle("✅ " + GetText(logChannel.Guild, strs.user_joined)) - .WithDescription($"{usr.Mention} `{usr}`") - .AddField("Id", usr.Id.ToString()) - .AddField(GetText(logChannel.Guild, strs.joined_server), - $"{usr.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", - true) - .AddField(GetText(logChannel.Guild, strs.joined_discord), - $"{usr.CreatedAt:dd.MM.yyyy HH:mm}", - true) - .WithFooter(CurrentTime(usr.Guild)); - - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); - - await logChannel.EmbedAsync(embed); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserUnbanned(IUser usr, IGuild guild) - { - _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(guild.Id, out var logSetting) - || logSetting.UserUnbannedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned)) is null) - return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned)) - .WithDescription(usr.ToString()!) - .AddField("Id", usr.Id.ToString()) - .WithFooter(CurrentTime(guild)); - - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); - - await logChannel.EmbedAsync(embed); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserBanned(IUser usr, IGuild guild) - { - _ = Task.Run(async () => - { - try - { - if (!GuildLogSettings.TryGetValue(guild.Id, out var logSetting) - || logSetting.UserBannedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserBanned)) == null) - return; - - - string? reason = null; - try - { - var ban = await guild.GetBanAsync(usr); - reason = ban?.Reason; - } - catch - { - } - - var embed = _eb.Create() - .WithOkColor() - .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned)) - .WithDescription(usr.ToString()!) - .AddField("Id", usr.Id.ToString()) - .AddField("Reason", string.IsNullOrWhiteSpace(reason) ? "-" : reason) - .WithFooter(CurrentTime(guild)); - - var avatarUrl = usr.GetAvatarUrl(); - - if (Uri.IsWellFormedUriString(avatarUrl, UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); - - await logChannel.EmbedAsync(embed); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_MessageDeleted(Cacheable optMsg, Cacheable optCh) - { - _ = Task.Run(async () => - { - try - { - if (optMsg.Value is not IUserMessage msg || msg.IsAuthor(_client)) - return; - - if (_ignoreMessageIds.Contains(msg.Id)) - return; - - var ch = optCh.Value; - if (ch is not ITextChannel channel) - return; - - if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out var logSetting) - || logSetting.MessageDeletedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageDeleted)) is null - || logChannel.Id == msg.Id) - return; - - var resolvedMessage = msg.Resolve(TagHandling.FullName); - var embed = _eb.Create() - .WithOkColor() - .WithTitle("🗑 " - + GetText(logChannel.Guild, strs.msg_del(((ITextChannel)msg.Channel).Name))) - .WithDescription(msg.Author.ToString()!) - .AddField(GetText(logChannel.Guild, strs.content), - string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage) - .AddField("Id", msg.Id.ToString()) - .WithFooter(CurrentTime(channel.Guild)); - if (msg.Attachments.Any()) - { - embed.AddField(GetText(logChannel.Guild, strs.attachments), - string.Join(", ", msg.Attachments.Select(a => a.Url))); - } - - await logChannel.EmbedAsync(embed); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_MessageUpdated( - Cacheable optmsg, - SocketMessage imsg2, - ISocketMessageChannel ch) - { - _ = Task.Run(async () => - { - try - { - if (imsg2 is not IUserMessage after || after.IsAuthor(_client)) - return; - - if ((optmsg.HasValue ? optmsg.Value : null) is not IUserMessage before) - return; - - if (ch is not ITextChannel channel) - return; - - if (before.Content == after.Content) - return; - - if (before.Author.IsBot) - return; - - if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out var logSetting) - || logSetting.MessageUpdatedId is null - || logSetting.LogIgnores.Any(ilc - => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; - - ITextChannel? logChannel; - if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageUpdated)) is null - || logChannel.Id == after.Channel.Id) - return; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle("📝 " - + GetText(logChannel.Guild, - strs.msg_update(((ITextChannel)after.Channel).Name))) - .WithDescription(after.Author.ToString()!) - .AddField(GetText(logChannel.Guild, strs.old_msg), - string.IsNullOrWhiteSpace(before.Content) - ? "-" - : before.Resolve(TagHandling.FullName)) - .AddField(GetText(logChannel.Guild, strs.new_msg), - string.IsNullOrWhiteSpace(after.Content) ? "-" : after.Resolve(TagHandling.FullName)) - .AddField("Id", after.Id.ToString()) - .WithFooter(CurrentTime(channel.Guild)); - - await logChannel.EmbedAsync(embed); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private async Task TryGetLogChannel(IGuild guild, LogSetting logSetting, LogType logChannelType) - { - ulong? id = null; - switch (logChannelType) - { - case LogType.Other: - id = logSetting.LogOtherId; - break; - case LogType.MessageUpdated: - id = logSetting.MessageUpdatedId; - break; - case LogType.MessageDeleted: - id = logSetting.MessageDeletedId; - break; - case LogType.UserJoined: - id = logSetting.UserJoinedId; - break; - case LogType.UserLeft: - id = logSetting.UserLeftId; - break; - case LogType.UserBanned: - id = logSetting.UserBannedId; - break; - case LogType.UserUnbanned: - id = logSetting.UserUnbannedId; - break; - case LogType.UserUpdated: - id = logSetting.UserUpdatedId; - break; - case LogType.ChannelCreated: - id = logSetting.ChannelCreatedId; - break; - case LogType.ChannelDestroyed: - id = logSetting.ChannelDestroyedId; - break; - case LogType.ChannelUpdated: - id = logSetting.ChannelUpdatedId; - break; - case LogType.UserPresence: - id = logSetting.LogUserPresenceId; - break; - case LogType.VoicePresence: - id = logSetting.LogVoicePresenceId; - break; - case LogType.VoicePresenceTts: - id = logSetting.LogVoicePresenceTTSId; - break; - case LogType.UserMuted: - id = logSetting.UserMutedId; - break; - case LogType.UserWarned: - id = logSetting.LogWarnsId; - break; - case LogType.ThreadCreated: - id = logSetting.ThreadCreatedId; - break; - case LogType.ThreadDeleted: - id = logSetting.ThreadDeletedId; - break; - } - - if (id is null or 0) - { - UnsetLogSetting(guild.Id, logChannelType); - return null; - } - - var channel = await guild.GetTextChannelAsync(id.Value); - - if (channel is null) - { - UnsetLogSetting(guild.Id, logChannelType); - return null; - } - - return channel; - } - - private void UnsetLogSetting(ulong guildId, LogType logChannelType) - { - using var uow = _db.GetDbContext(); - var newLogSetting = uow.LogSettingsFor(guildId); - switch (logChannelType) - { - case LogType.Other: - newLogSetting.LogOtherId = null; - break; - case LogType.MessageUpdated: - newLogSetting.MessageUpdatedId = null; - break; - case LogType.MessageDeleted: - newLogSetting.MessageDeletedId = null; - break; - case LogType.UserJoined: - newLogSetting.UserJoinedId = null; - break; - case LogType.UserLeft: - newLogSetting.UserLeftId = null; - break; - case LogType.UserBanned: - newLogSetting.UserBannedId = null; - break; - case LogType.UserUnbanned: - newLogSetting.UserUnbannedId = null; - break; - case LogType.UserUpdated: - newLogSetting.UserUpdatedId = null; - break; - case LogType.UserMuted: - newLogSetting.UserMutedId = null; - break; - case LogType.ChannelCreated: - newLogSetting.ChannelCreatedId = null; - break; - case LogType.ChannelDestroyed: - newLogSetting.ChannelDestroyedId = null; - break; - case LogType.ChannelUpdated: - newLogSetting.ChannelUpdatedId = null; - break; - case LogType.UserPresence: - newLogSetting.LogUserPresenceId = null; - break; - case LogType.VoicePresence: - newLogSetting.LogVoicePresenceId = null; - break; - case LogType.VoicePresenceTts: - newLogSetting.LogVoicePresenceTTSId = null; - break; - case LogType.UserWarned: - newLogSetting.LogWarnsId = null; - break; - } - - GuildLogSettings.AddOrUpdate(guildId, newLogSetting, (_, _) => newLogSetting); - uow.SaveChanges(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommands.cs b/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommands.cs deleted file mode 100644 index 2035c24..0000000 --- a/src/Ellie.Bot.Modules.Administration/ServerLog/ServerLogCommands.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Ellie.Common.TypeReaders.Models; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - [NoPublicBot] - public partial class LogCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogServer(PermissionAction action) - { - await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value); - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.log_all); - else - await ReplyConfirmLocalizedAsync(strs.log_disabled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore() - { - var settings = _service.GetGuildLogSettings(ctx.Guild.Id); - - var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList() - ?? new List(); - var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList() - ?? new List(); - - var eb = _eb.Create(ctx) - .WithOkColor() - .AddField(GetText(strs.log_ignored_channels), - chs.Count == 0 - ? "-" - : string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>"))) - .AddField(GetText(strs.log_ignored_users), - usrs.Count == 0 - ? "-" - : string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>"))); - - await ctx.Channel.EmbedAsync(eb); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore([Leftover] ITextChannel target) - { - var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel); - - if (!removed) - { - await ReplyConfirmLocalizedAsync( - strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))); - } - else - { - await ReplyConfirmLocalizedAsync( - strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore([Leftover] IUser target) - { - var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User); - - if (!removed) - { - await ReplyConfirmLocalizedAsync( - strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))); - } - else - { - await ReplyConfirmLocalizedAsync( - strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogEvents() - { - var logSetting = _service.GetGuildLogSettings(ctx.Guild.Id); - var str = string.Join("\n", - Enum.GetNames() - .Select(x => - { - var val = logSetting is null ? null : GetLogProperty(logSetting, Enum.Parse(x)); - if (val is not null) - return $"{Format.Bold(x)} <#{val}>"; - return Format.Bold(x); - })); - - await SendConfirmAsync(Format.Bold(GetText(strs.log_events)) + "\n" + str); - } - - private static ulong? GetLogProperty(LogSetting l, LogType type) - { - switch (type) - { - case LogType.Other: - return l.LogOtherId; - case LogType.MessageUpdated: - return l.MessageUpdatedId; - case LogType.MessageDeleted: - return l.MessageDeletedId; - case LogType.UserJoined: - return l.UserJoinedId; - case LogType.UserLeft: - return l.UserLeftId; - case LogType.UserBanned: - return l.UserBannedId; - case LogType.UserUnbanned: - return l.UserUnbannedId; - case LogType.UserUpdated: - return l.UserUpdatedId; - case LogType.ChannelCreated: - return l.ChannelCreatedId; - case LogType.ChannelDestroyed: - return l.ChannelDestroyedId; - case LogType.ChannelUpdated: - return l.ChannelUpdatedId; - case LogType.UserPresence: - return l.LogUserPresenceId; - case LogType.VoicePresence: - return l.LogVoicePresenceId; - case LogType.VoicePresenceTts: - return l.LogVoicePresenceTTSId; - case LogType.UserMuted: - return l.UserMutedId; - case LogType.UserWarned: - return l.LogWarnsId; - case LogType.ThreadDeleted: - return l.ThreadDeletedId; - case LogType.ThreadCreated: - return l.ThreadCreatedId; - default: - return null; - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task Log(LogType type) - { - var val = _service.Log(ctx.Guild.Id, ctx.Channel.Id, type); - - if (val) - await ReplyConfirmLocalizedAsync(strs.log(Format.Bold(type.ToString()))); - else - await ReplyConfirmLocalizedAsync(strs.log_stop(Format.Bold(type.ToString()))); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs b/src/Ellie.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs deleted file mode 100644 index 0bd504e..0000000 --- a/src/Ellie.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable disable -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public sealed class GuildTimezoneService : ITimezoneService, IEService -{ - public static ConcurrentDictionary AllServices { get; } = new(); - private readonly ConcurrentDictionary _timezones; - private readonly DbService _db; - - public GuildTimezoneService(DiscordSocketClient client, IBot bot, DbService db) - { - _timezones = bot.AllGuildConfigs.Select(GetTimzezoneTuple) - .Where(x => x.Timezone is not null) - .ToDictionary(x => x.GuildId, x => x.Timezone) - .ToConcurrent(); - - var curUser = client.CurrentUser; - if (curUser is not null) - AllServices.TryAdd(curUser.Id, this); - _db = db; - - bot.JoinedGuild += Bot_JoinedGuild; - } - - private Task Bot_JoinedGuild(GuildConfig arg) - { - var (guildId, tz) = GetTimzezoneTuple(arg); - if (tz is not null) - _timezones.TryAdd(guildId, tz); - return Task.CompletedTask; - } - - private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x) - { - TimeZoneInfo tz; - try - { - if (x.TimeZoneId is null) - tz = null; - else - tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); - } - catch - { - tz = null; - } - - return (x.GuildId, Timezone: tz); - } - - public TimeZoneInfo GetTimeZoneOrDefault(ulong? guildId) - { - if (guildId is ulong gid && _timezones.TryGetValue(gid, out var tz)) - return tz; - - return null; - } - - public void SetTimeZone(ulong guildId, TimeZoneInfo tz) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - - gc.TimeZoneId = tz?.Id; - uow.SaveChanges(); - - if (tz is null) - _timezones.TryRemove(guildId, out tz); - else - _timezones.AddOrUpdate(guildId, tz, (_, _) => tz); - } - - public TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId) - => GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/Timezone/TimeZoneCommands.cs b/src/Ellie.Bot.Modules.Administration/Timezone/TimeZoneCommands.cs deleted file mode 100644 index 2386619..0000000 --- a/src/Ellie.Bot.Modules.Administration/Timezone/TimeZoneCommands.cs +++ /dev/null @@ -1,75 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class TimeZoneCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Timezones(int page = 1) - { - page--; - - if (page is < 0 or > 20) - return; - - var timezones = TimeZoneInfo.GetSystemTimeZones().OrderBy(x => x.BaseUtcOffset).ToArray(); - var timezonesPerPage = 20; - - var curTime = DateTimeOffset.UtcNow; - - var i = 0; - var timezoneStrings = timezones.Select(x => (x, ++i % 2 == 0)) - .Select(data => - { - var (tzInfo, flip) = data; - var nameStr = $"{tzInfo.Id,-30}"; - var offset = curTime.ToOffset(tzInfo.GetUtcOffset(curTime)) - .ToString("zzz"); - if (flip) - return $"{offset} {Format.Code(nameStr)}"; - return $"{Format.Code(offset)} {nameStr}"; - }); - - - await ctx.SendPaginatedConfirmAsync(page, - curPage => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.timezones_available)) - .WithDescription(string.Join("\n", - timezoneStrings.Skip(curPage * timezonesPerPage).Take(timezonesPerPage))), - timezones.Length, - timezonesPerPage); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Timezone() - => await ReplyConfirmLocalizedAsync(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id))); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task Timezone([Leftover] string id) - { - TimeZoneInfo tz; - try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); } - catch { tz = null; } - - - if (tz is null) - { - await ReplyErrorLocalizedAsync(strs.timezone_not_found); - return; - } - - _service.SetTimeZone(ctx.Guild.Id, tz); - - await SendConfirmAsync(tz.ToString()); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs b/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs deleted file mode 100644 index b8fd444..0000000 --- a/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs +++ /dev/null @@ -1,932 +0,0 @@ -#nullable disable -using CommandLine; -using Humanizer.Localisation; -using Ellie.Common.TypeReaders.Models; -using Ellie.Modules.Administration.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class UserPunishCommands : EllieModule - { - public enum AddRole - { - AddRole - } - - private readonly MuteService _mute; - - public UserPunishCommands(MuteService mute) - { - _mute = mute; - } - - private async Task CheckRoleHierarchy(IGuildUser target) - { - var curUser = ((SocketGuild)ctx.Guild).CurrentUser; - var ownerId = ctx.Guild.OwnerId; - var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position); - var targetMaxRole = target.GetRoles().Max(r => r.Position); - var botMaxRole = curUser.GetRoles().Max(r => r.Position); - // bot can't punish a user who is higher in the hierarchy. Discord will return 403 - // moderator can be owner, in which case role hierarchy doesn't matter - // otherwise, moderator has to have a higher role - if (botMaxRole <= targetMaxRole - || (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole) - || target.Id == ownerId) - { - await ReplyErrorLocalizedAsync(strs.hierarchy); - return false; - } - - return true; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public Task Warn(IGuildUser user, [Leftover] string reason = null) - => Warn(1, user, reason); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null) - { - if (weight <= 0) - return; - - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - try - { - await user.EmbedAsync(_eb.Create() - .WithErrorColor() - .WithDescription(GetText(strs.warned_on(ctx.Guild.ToString()))) - .AddField(GetText(strs.moderator), ctx.User.ToString()) - .AddField(GetText(strs.reason), reason ?? "-")); - } - catch - { - dmFailed = true; - } - - WarningPunishment punishment; - try - { - punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason); - } - catch (Exception ex) - { - Log.Warning(ex, "Exception occured while warning a user"); - var errorEmbed = _eb.Create().WithErrorColor().WithDescription(GetText(strs.cant_apply_punishment)); - - if (dmFailed) - errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(errorEmbed); - return; - } - - var embed = _eb.Create().WithOkColor(); - if (punishment is null) - embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString())))); - else - { - embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()), - Format.Bold(punishment.Punishment.ToString())))); - } - - if (dmFailed) - embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [EllieOptions] - [Priority(1)] - public async Task WarnExpire() - { - var expireDays = await _service.GetWarnExpire(ctx.Guild.Id); - - if (expireDays == 0) - await ReplyConfirmLocalizedAsync(strs.warns_dont_expire); - else - await ReplyErrorLocalizedAsync(strs.warns_expire_in(expireDays)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [EllieOptions] - [Priority(2)] - public async Task WarnExpire(int days, params string[] args) - { - if (days is < 0 or > 366) - return; - - var opts = OptionsParser.ParseFrom(args); - - await ctx.Channel.TriggerTypingAsync(); - - await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete); - if (days == 0) - { - await ReplyConfirmLocalizedAsync(strs.warn_expire_reset); - return; - } - - if (opts.Delete) - await ReplyConfirmLocalizedAsync(strs.warn_expire_set_delete(Format.Bold(days.ToString()))); - else - await ReplyConfirmLocalizedAsync(strs.warn_expire_set_clear(Format.Bold(days.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(2)] - public Task Warnlog(int page, [Leftover] IGuildUser user = null) - { - user ??= (IGuildUser)ctx.User; - - return Warnlog(page, user.Id); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(3)] - public Task Warnlog(IGuildUser user = null) - { - user ??= (IGuildUser)ctx.User; - - return ctx.User.Id == user.Id || ((IGuildUser)ctx.User).GuildPermissions.BanMembers - ? Warnlog(user.Id) - : Task.CompletedTask; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(0)] - public Task Warnlog(int page, ulong userId) - => InternalWarnlog(userId, page - 1); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(1)] - public Task Warnlog(ulong userId) - => InternalWarnlog(userId, 0); - - private async Task InternalWarnlog(ulong userId, int inputPage) - { - if (inputPage < 0) - return; - - var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId); - - await ctx.SendPaginatedConfirmAsync(inputPage, - page => - { - var warnings = allWarnings.Skip(page * 9).Take(9).ToArray(); - - var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString(); - var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.warnlog_for(user))); - - if (!warnings.Any()) - embed.WithDescription(GetText(strs.warnings_none)); - else - { - var descText = GetText(strs.warn_count( - Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()), - Format.Bold(warnings.Sum(x => x.Weight).ToString()))); - - embed.WithDescription(descText); - - var i = page * 9; - foreach (var w in warnings) - { - i++; - var name = GetText(strs.warned_on_by(w.DateAdded?.ToString("dd.MM.yyy"), - w.DateAdded?.ToString("HH:mm"), - w.Moderator)); - - if (w.Forgiven) - name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}"; - - - embed.AddField($"#`{i}` " + name, - Format.Code(GetText(strs.warn_weight(w.Weight))) + '\n' + w.Reason.TrimTo(1000)); - } - } - - return embed; - }, - allWarnings.Length, - 9); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnlogAll(int page = 1) - { - if (--page < 0) - return; - var warnings = _service.WarnlogAll(ctx.Guild.Id); - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var ws = warnings.Skip(curPage * 15) - .Take(15) - .ToArray() - .Select(x => - { - var all = x.Count(); - var forgiven = x.Count(y => y.Forgiven); - var total = all - forgiven; - var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key); - return (usr?.ToString() ?? x.Key.ToString()) - + $" | {total} ({all} - {forgiven})"; - }); - - return _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.warnings_list)) - .WithDescription(string.Join("\n", ws)); - }, - warnings.Length, - 15); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public Task Warnclear(IGuildUser user, int index = 0) - => Warnclear(user.Id, index); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task Warnclear(ulong userId, int index = 0) - { - if (index < 0) - return; - var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString()); - var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString()); - if (index == 0) - await ReplyErrorLocalizedAsync(strs.warnings_cleared(userStr)); - else - { - if (success) - await ReplyConfirmLocalizedAsync(strs.warning_cleared(Format.Bold(index.ToString()), userStr)); - else - await ReplyErrorLocalizedAsync(strs.warning_clear_fail); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(1)] - public async Task WarnPunish( - int number, - AddRole _, - IRole role, - StoopidTime time = null) - { - var punish = PunishmentAction.AddRole; - - if (ctx.Guild.OwnerId != ctx.User.Id - && role.Position >= ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.role_too_high); - return; - } - - var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role); - - if (!success) - return; - - if (time is null) - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()), - Format.Bold(number.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), - Format.Bold(number.ToString()), - Format.Bold(time.Input))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null) - { - // this should never happen. Addrole has its own method with higher priority - // also disallow warn punishment for getting warned - if (punish is PunishmentAction.AddRole or PunishmentAction.Warn) - return; - - // you must specify the time for timeout - if (punish is PunishmentAction.TimeOut && time is null) - return; - - var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time); - - if (!success) - return; - - if (time is null) - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()), - Format.Bold(number.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), - Format.Bold(number.ToString()), - Format.Bold(time.Input))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnPunish(int number) - { - if (!_service.WarnPunishRemove(ctx.Guild.Id, number)) - return; - - await ReplyConfirmLocalizedAsync(strs.warn_punish_rem(Format.Bold(number.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task WarnPunishList() - { - var ps = _service.WarnPunishList(ctx.Guild.Id); - - string list; - if (ps.Any()) - { - list = string.Join("\n", - ps.Select(x - => $"{x.Count} -> {x.Punishment} {(x.Punishment == PunishmentAction.AddRole ? $"<@&{x.RoleId}>" : "")} {(x.Time <= 0 ? "" : x.Time + "m")} ")); - } - else - list = GetText(strs.warnpl_none); - - await SendConfirmAsync(GetText(strs.warn_punish_list), list); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(1)] - public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null) - => Ban(time, user.Id, msg); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(0)] - public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null) - { - if (time.Time > TimeSpan.FromDays(49)) - return; - - var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - - - if (guildUser is not null && !await CheckRoleHierarchy(guildUser)) - return; - - var dmFailed = false; - - if (guildUser is not null) - { - try - { - var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); - var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time); - if (embed is not null) - await guildUser.SendAsync(embed); - } - catch - { - dmFailed = true; - } - } - - var user = await ctx.Client.GetUserAsync(userId); - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune); - var toSend = _eb.Create() - .WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true) - .AddField("ID", userId.ToString(), true) - .AddField(GetText(strs.duration), - time.Time.Humanize(3, minUnit: TimeUnit.Minute, culture: Culture), - true); - - if (dmFailed) - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(0)] - public async Task Ban(ulong userId, [Leftover] string msg = null) - { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - { - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512)); - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField("ID", userId.ToString(), true)); - } - else - await Ban(user, msg); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(2)] - public async Task Ban(IGuildUser user, [Leftover] string msg = null) - { - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - - try - { - var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); - var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null); - if (embed is not null) - await user.SendAsync(embed); - } - catch - { - dmFailed = true; - } - - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512)); - - var toSend = _eb.Create() - .WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task BanPrune(int days) - { - if (days < 0 || days > 7) - { - await ReplyErrorLocalizedAsync(strs.invalid_input); - return; - } - - await _service.SetBanPruneAsync(ctx.Guild.Id, days); - - if (days == 0) - await ReplyConfirmLocalizedAsync(strs.ban_prune_disabled); - else - await ReplyConfirmLocalizedAsync(strs.ban_prune(days)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task BanMessage([Leftover] string message = null) - { - if (message is null) - { - var template = _service.GetBanTemplate(ctx.Guild.Id); - if (template is null) - { - await ReplyConfirmLocalizedAsync(strs.banmsg_default); - return; - } - - await SendConfirmAsync(template); - return; - } - - _service.SetBanTemplate(ctx.Guild.Id, message); - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task BanMsgReset() - { - _service.SetBanTemplate(ctx.Guild.Id, null); - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(0)] - public Task BanMessageTest([Leftover] string reason = null) - => InternalBanMessageTest(reason, null); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(1)] - public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null) - => InternalBanMessageTest(reason, duration.Time); - - private async Task InternalBanMessageTest(string reason, TimeSpan? duration) - { - var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason)); - var embed = _service.GetBanUserDmEmbed(Context, (IGuildUser)ctx.User, defaultMessage, reason, duration); - - if (embed is null) - await ConfirmLocalizedAsync(strs.banmsg_disabled); - else - { - try - { - await ctx.User.SendAsync(embed); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.unable_to_dm_user); - return; - } - - await ctx.OkAsync(); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Unban([Leftover] string user) - { - var bans = await ctx.Guild.GetBansAsync().FlattenAsync(); - - var bun = bans.FirstOrDefault(x => x.User.ToString()!.ToLowerInvariant() == user.ToLowerInvariant()); - - if (bun is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found); - return; - } - - await UnbanInternal(bun.User); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Unban(ulong userId) - { - var bun = await ctx.Guild.GetBanAsync(userId); - - if (bun is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found); - return; - } - - await UnbanInternal(bun.User); - } - - private async Task UnbanInternal(IUser user) - { - await ctx.Guild.RemoveBanAsync(user); - - await ReplyConfirmLocalizedAsync(strs.unbanned_user(Format.Bold(user.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.BanMembers)] - public Task Softban(IGuildUser user, [Leftover] string msg = null) - => SoftbanInternal(user, msg); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Softban(ulong userId, [Leftover] string msg = null) - { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - return; - - await SoftbanInternal(user, msg); - } - - private async Task SoftbanInternal(IGuildUser user, [Leftover] string msg = null) - { - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - - try - { - await user.SendErrorAsync(_eb, GetText(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))); - } - catch - { - dmFailed = true; - } - - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - await ctx.Guild.AddBanAsync(user, banPrune, ("Softban | " + ctx.User + " | " + msg).TrimTo(512)); - try { await ctx.Guild.RemoveBanAsync(user); } - catch { await ctx.Guild.RemoveBanAsync(user); } - - var toSend = _eb.Create() - .WithOkColor() - .WithTitle("☣ " + GetText(strs.sb_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers)] - [BotPerm(GuildPerm.KickMembers)] - [Priority(1)] - public Task Kick(IGuildUser user, [Leftover] string msg = null) - => KickInternal(user, msg); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers)] - [BotPerm(GuildPerm.KickMembers)] - [Priority(0)] - public async Task Kick(ulong userId, [Leftover] string msg = null) - { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - return; - - await KickInternal(user, msg); - } - - private async Task KickInternal(IGuildUser user, string msg = null) - { - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - - try - { - await user.SendErrorAsync(_eb, GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg))); - } - catch - { - dmFailed = true; - } - - await user.KickAsync((ctx.User + " | " + msg).TrimTo(512)); - - var toSend = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.kicked_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ModerateMembers)] - [BotPerm(GuildPerm.ModerateMembers)] - [Priority(2)] - public async Task Timeout(IUser globalUser, StoopidTime time, [Leftover] string msg = null) - { - var user = await ctx.Guild.GetUserAsync(globalUser.Id); - - if (user is null) - return; - - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - - try - { - var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg)); - await user.EmbedAsync(_eb.Create(ctx) - .WithPendingColor() - .WithDescription(dmMessage)); - } - catch - { - dmFailed = true; - } - - await user.SetTimeOutAsync(time.Time); - - var toSend = _eb.Create() - .WithOkColor() - .WithTitle("⏳ " + GetText(strs.timedout_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - - await ctx.Channel.EmbedAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Ratelimit(30)] - public async Task MassBan(params string[] userStrings) - { - if (userStrings.Length == 0) - return; - - var missing = new List(); - var banning = new HashSet(); - - await ctx.Channel.TriggerTypingAsync(); - foreach (var userStr in userStrings) - { - if (ulong.TryParse(userStr, out var userId)) - { - IUser user = await ctx.Guild.GetUserAsync(userId) - ?? await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, - userId); - - if (user is null) - { - // if IGuildUser is null, try to get IUser - user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId); - - // only add to missing if *still* null - if (user is null) - { - missing.Add(userStr); - continue; - } - } - - //Hierachy checks only if the user is in the guild - if (user is IGuildUser gu && !await CheckRoleHierarchy(gu)) - return; - - banning.Add(user); - } - else - missing.Add(userStr); - } - - var missStr = string.Join("\n", missing); - if (string.IsNullOrWhiteSpace(missStr)) - missStr = "-"; - - var toSend = _eb.Create(ctx) - .WithDescription(GetText(strs.mass_ban_in_progress(banning.Count))) - .AddField(GetText(strs.invalid(missing.Count)), missStr) - .WithPendingColor(); - - var banningMessage = await ctx.Channel.EmbedAsync(toSend); - - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - foreach (var toBan in banning) - { - try - { - await ctx.Guild.AddBanAsync(toBan.Id, banPrune, $"{ctx.User} | Massban"); - } - catch (Exception ex) - { - Log.Warning(ex, "Error banning {User} user in {GuildId} server", toBan.Id, ctx.Guild.Id); - } - } - - await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() - .WithDescription( - GetText(strs.mass_ban_completed(banning.Count()))) - .AddField(GetText(strs.invalid(missing.Count)), missStr) - .WithOkColor() - .Build()); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [OwnerOnly] - public async Task MassKill([Leftover] string people) - { - if (string.IsNullOrWhiteSpace(people)) - return; - - var (bans, missing) = _service.MassKill((SocketGuild)ctx.Guild, people); - - var missStr = string.Join("\n", missing); - if (string.IsNullOrWhiteSpace(missStr)) - missStr = "-"; - - //send a message but don't wait for it - var banningMessageTask = ctx.Channel.EmbedAsync(_eb.Create() - .WithDescription( - GetText(strs.mass_kill_in_progress(bans.Count()))) - .AddField(GetText(strs.invalid(missing)), missStr) - .WithPendingColor()); - - var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; - //do the banning - await Task.WhenAll(bans.Where(x => x.Id.HasValue) - .Select(x => ctx.Guild.AddBanAsync(x.Id.Value, - banPrune, - x.Reason, - new() - { - RetryMode = RetryMode.AlwaysRetry - }))); - - //wait for the message and edit it - var banningMessage = await banningMessageTask; - - await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() - .WithDescription( - GetText(strs.mass_kill_completed(bans.Count()))) - .AddField(GetText(strs.invalid(missing)), missStr) - .WithOkColor() - .Build()); - } - - public class WarnExpireOptions : IEllieCommandOptions - { - [Option('d', "delete", Default = false, HelpText = "Delete warnings instead of clearing them.")] - public bool Delete { get; set; } = false; - - public void NormalizeOptions() - { - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishService.cs b/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishService.cs deleted file mode 100644 index aecd109..0000000 --- a/src/Ellie.Bot.Modules.Administration/UserPunish/UserPunishService.cs +++ /dev/null @@ -1,595 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Common.TypeReaders.Models; -using Ellie.Db; -using Ellie.Modules.Permissions.Services; -using Ellie.Services.Database.Models; -using Newtonsoft.Json; - -namespace Ellie.Modules.Administration.Services; - -public class UserPunishService : IEService, IReadyExecutor -{ - private readonly MuteService _mute; - private readonly DbService _db; - private readonly BlacklistService _blacklistService; - private readonly BotConfigService _bcs; - private readonly DiscordSocketClient _client; - - public event Func OnUserWarned = static delegate { return Task.CompletedTask; }; - - public UserPunishService( - MuteService mute, - DbService db, - BlacklistService blacklistService, - BotConfigService bcs, - DiscordSocketClient client) - { - _mute = mute; - _db = db; - _blacklistService = blacklistService; - _bcs = bcs; - _client = client; - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var expiryTimer = new PeriodicTimer(TimeSpan.FromHours(12)); - do - { - try - { - await CheckAllWarnExpiresAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "Unexpected error while checking for warn expiries: {ErrorMessage}", ex.Message); - } - } while (await expiryTimer.WaitForNextTickAsync()); - } - - public async Task Warn( - IGuild guild, - ulong userId, - IUser mod, - long weight, - string reason) - { - if (weight <= 0) - throw new ArgumentOutOfRangeException(nameof(weight)); - - var modName = mod.ToString(); - - if (string.IsNullOrWhiteSpace(reason)) - reason = "-"; - - var guildId = guild.Id; - - var warn = new Warning - { - UserId = userId, - GuildId = guildId, - Forgiven = false, - Reason = reason, - Moderator = modName, - Weight = weight - }; - - long previousCount; - List ps; - await using (var uow = _db.GetDbContext()) - { - ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - - previousCount = uow.Set().ForId(guildId, userId) - .Where(w => !w.Forgiven && w.UserId == userId) - .Sum(x => x.Weight); - - uow.Set().Add(warn); - - await uow.SaveChangesAsync(); - } - - _ = OnUserWarned(warn); - - var totalCount = previousCount + weight; - - var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount) - .MaxBy(x => x.Count); - - if (p is not null) - { - var user = await guild.GetUserAsync(userId); - if (user is null) - return null; - - await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times."); - return p; - } - - return null; - } - - public async Task ApplyPunishment( - IGuild guild, - IGuildUser user, - IUser mod, - PunishmentAction p, - int minutes, - ulong? roleId, - string reason) - { - if (!await CheckPermission(guild, p)) - return; - - int banPrune; - switch (p) - { - case PunishmentAction.Mute: - if (minutes == 0) - await _mute.MuteUser(user, mod, reason: reason); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason); - break; - case PunishmentAction.VoiceMute: - if (minutes == 0) - await _mute.MuteUser(user, mod, MuteType.Voice, reason); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason); - break; - case PunishmentAction.ChatMute: - if (minutes == 0) - await _mute.MuteUser(user, mod, MuteType.Chat, reason); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason); - break; - case PunishmentAction.Kick: - await user.KickAsync(reason); - break; - case PunishmentAction.Ban: - banPrune = await GetBanPruneAsync(user.GuildId) ?? 7; - if (minutes == 0) - await guild.AddBanAsync(user, reason: reason, pruneDays: banPrune); - else - await _mute.TimedBan(user.Guild, user.Id, TimeSpan.FromMinutes(minutes), reason, banPrune); - break; - case PunishmentAction.Softban: - banPrune = await GetBanPruneAsync(user.GuildId) ?? 7; - await guild.AddBanAsync(user, banPrune, $"Softban | {reason}"); - try - { - await guild.RemoveBanAsync(user); - } - catch - { - await guild.RemoveBanAsync(user); - } - - break; - case PunishmentAction.RemoveRoles: - await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole)); - break; - case PunishmentAction.AddRole: - if (roleId is null) - return; - var role = guild.GetRole(roleId.Value); - if (role is not null) - { - if (minutes == 0) - await user.AddRoleAsync(role); - else - await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role); - } - else - { - Log.Warning("Can't find role {RoleId} on server {GuildId} to apply punishment", - roleId.Value, - guild.Id); - } - - break; - case PunishmentAction.Warn: - await Warn(guild, user.Id, mod, 1, reason); - break; - case PunishmentAction.TimeOut: - await user.SetTimeOutAsync(TimeSpan.FromMinutes(minutes)); - break; - } - } - - /// - /// Used to prevent the bot from hitting 403's when it needs to - /// apply punishments with insufficient permissions - /// - /// Guild the punishment is applied in - /// Punishment to apply - /// Whether the bot has sufficient permissions - private async Task CheckPermission(IGuild guild, PunishmentAction punish) - { - var botUser = await guild.GetCurrentUserAsync(); - switch (punish) - { - case PunishmentAction.Mute: - return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles; - case PunishmentAction.Kick: - return botUser.GuildPermissions.KickMembers; - case PunishmentAction.Ban: - return botUser.GuildPermissions.BanMembers; - case PunishmentAction.Softban: - return botUser.GuildPermissions.BanMembers; // ban + unban - case PunishmentAction.RemoveRoles: - return botUser.GuildPermissions.ManageRoles; - case PunishmentAction.ChatMute: - return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role - case PunishmentAction.VoiceMute: - return botUser.GuildPermissions.MuteMembers; - case PunishmentAction.AddRole: - return botUser.GuildPermissions.ManageRoles; - case PunishmentAction.TimeOut: - return botUser.GuildPermissions.ModerateMembers; - default: - return true; - } - } - - public async Task CheckAllWarnExpiresAsync() - { - await using var uow = _db.GetDbContext(); - var cleared = await uow.Set() - .Where(x => uow.Set() - .Any(y => y.GuildId == x.GuildId - && y.WarnExpireHours > 0 - && y.WarnExpireAction == WarnExpireAction.Clear) - && x.Forgiven == false - && x.DateAdded - < DateTime.UtcNow.AddHours(-uow.Set() - .Where(y => x.GuildId == y.GuildId) - .Select(y => y.WarnExpireHours) - .First())) - .UpdateAsync(_ => new() - { - Forgiven = true, - ForgivenBy = "expiry" - }); - - var deleted = await uow.Set() - .Where(x => uow.Set() - .Any(y => y.GuildId == x.GuildId - && y.WarnExpireHours > 0 - && y.WarnExpireAction == WarnExpireAction.Delete) - && x.DateAdded - < DateTime.UtcNow.AddHours(-uow.Set() - .Where(y => x.GuildId == y.GuildId) - .Select(y => y.WarnExpireHours) - .First())) - .DeleteAsync(); - - if (cleared > 0 || deleted > 0) - { - Log.Information("Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry", - cleared, - deleted); - } - - await uow.SaveChangesAsync(); - } - - public async Task CheckWarnExpiresAsync(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, inc => inc); - - if (config.WarnExpireHours == 0) - return; - - if (config.WarnExpireAction == WarnExpireAction.Clear) - { - await uow.Set() - .Where(x => x.GuildId == guildId - && x.Forgiven == false - && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) - .UpdateAsync(_ => new() - { - Forgiven = true, - ForgivenBy = "expiry" - }); - } - else if (config.WarnExpireAction == WarnExpireAction.Delete) - { - await uow.Set() - .Where(x => x.GuildId == guildId - && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) - .DeleteAsync(); - } - - await uow.SaveChangesAsync(); - } - - public Task GetWarnExpire(ulong guildId) - { - using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, set => set); - return Task.FromResult(config.WarnExpireHours / 24); - } - - public async Task WarnExpireAsync(ulong guildId, int days, bool delete) - { - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, inc => inc); - - config.WarnExpireHours = days * 24; - config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear; - await uow.SaveChangesAsync(); - - // no need to check for warn expires - if (config.WarnExpireHours == 0) - return; - } - - await CheckWarnExpiresAsync(guildId); - } - - public IGrouping[] WarnlogAll(ulong gid) - { - using var uow = _db.GetDbContext(); - return uow.Set().GetForGuild(gid).GroupBy(x => x.UserId).ToArray(); - } - - public Warning[] UserWarnings(ulong gid, ulong userId) - { - using var uow = _db.GetDbContext(); - return uow.Set().ForId(gid, userId); - } - - public async Task WarnClearAsync( - ulong guildId, - ulong userId, - int index, - string moderator) - { - var toReturn = true; - await using var uow = _db.GetDbContext(); - if (index == 0) - await uow.Set().ForgiveAll(guildId, userId, moderator); - else - toReturn = uow.Set().Forgive(guildId, userId, moderator, index - 1); - await uow.SaveChangesAsync(); - return toReturn; - } - - public bool WarnPunish( - ulong guildId, - int number, - PunishmentAction punish, - StoopidTime time, - IRole role = null) - { - // these 3 don't make sense with time - if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles - && time is not null) - return false; - if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49))) - return false; - - if (punish is PunishmentAction.AddRole && role is null) - return false; - - if (punish is PunishmentAction.TimeOut && time is null) - return false; - - using var uow = _db.GetDbContext(); - var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - var toDelete = ps.Where(x => x.Count == number); - - uow.RemoveRange(toDelete); - - ps.Add(new() - { - Count = number, - Punishment = punish, - Time = (int?)time?.Time.TotalMinutes ?? 0, - RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?) - }); - uow.SaveChanges(); - return true; - } - - public bool WarnPunishRemove(ulong guildId, int number) - { - if (number <= 0) - return false; - - using var uow = _db.GetDbContext(); - var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - var p = ps.FirstOrDefault(x => x.Count == number); - - if (p is not null) - { - uow.Remove(p); - uow.SaveChanges(); - } - - return true; - } - - public WarningPunishment[] WarnPunishList(ulong guildId) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments)) - .WarnPunishments.OrderBy(x => x.Count) - .ToArray(); - } - - public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill( - SocketGuild guild, - string people) - { - var gusers = guild.Users; - //get user objects and reasons - var bans = people.Split("\n") - .Select(x => - { - var split = x.Trim().Split(" "); - - var reason = string.Join(" ", split.Skip(1)); - - if (ulong.TryParse(split[0], out var id)) - return (Original: split[0], Id: id, Reason: reason); - - return (Original: split[0], - gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id, - Reason: reason); - }) - .ToArray(); - - //if user is null, means that person couldn't be found - var missing = bans.Count(x => !x.Id.HasValue); - - //get only data for found users - var found = bans.Where(x => x.Id.HasValue).Select(x => x.Id.Value).ToList(); - - _blacklistService.BlacklistUsers(found); - - return (bans, missing); - } - - public string GetBanTemplate(ulong guildId) - { - using var uow = _db.GetDbContext(); - var template = uow.Set().AsQueryable().FirstOrDefault(x => x.GuildId == guildId); - return template?.Text; - } - - public void SetBanTemplate(ulong guildId, string text) - { - using var uow = _db.GetDbContext(); - var template = uow.Set().AsQueryable().FirstOrDefault(x => x.GuildId == guildId); - - if (text is null) - { - if (template is null) - return; - - uow.Remove(template); - } - else if (template is null) - { - uow.Set().Add(new() - { - GuildId = guildId, - Text = text - }); - } - else - template.Text = text; - - uow.SaveChanges(); - } - - public async Task SetBanPruneAsync(ulong guildId, int? pruneDays) - { - await using var ctx = _db.GetDbContext(); - await ctx.Set() - .ToLinqToDBTable() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - Text = null, - DateAdded = DateTime.UtcNow, - PruneDays = pruneDays - }, - old => new() - { - PruneDays = pruneDays - }, - () => new() - { - GuildId = guildId - }); - } - - public async Task GetBanPruneAsync(ulong guildId) - { - await using var ctx = _db.GetDbContext(); - return await ctx.Set() - .Where(x => x.GuildId == guildId) - .Select(x => x.PruneDays) - .FirstOrDefaultAsyncLinqToDB(); - } - - public SmartText GetBanUserDmEmbed( - ICommandContext context, - IGuildUser target, - string defaultMessage, - string banReason, - TimeSpan? duration) - => GetBanUserDmEmbed((DiscordSocketClient)context.Client, - (SocketGuild)context.Guild, - (IGuildUser)context.User, - target, - defaultMessage, - banReason, - duration); - - public SmartText GetBanUserDmEmbed( - DiscordSocketClient client, - SocketGuild guild, - IGuildUser moderator, - IGuildUser target, - string defaultMessage, - string banReason, - TimeSpan? duration) - { - var template = GetBanTemplate(guild.Id); - - banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason; - - var replacer = new ReplacementBuilder().WithServer(client, guild) - .WithOverride("%ban.mod%", () => moderator.ToString()) - .WithOverride("%ban.mod.fullname%", () => moderator.ToString()) - .WithOverride("%ban.mod.name%", () => moderator.Username) - .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) - .WithOverride("%ban.user%", () => target.ToString()) - .WithOverride("%ban.user.fullname%", () => target.ToString()) - .WithOverride("%ban.user.name%", () => target.Username) - .WithOverride("%ban.user.discrim%", () => target.Discriminator) - .WithOverride("%reason%", () => banReason) - .WithOverride("%ban.reason%", () => banReason) - .WithOverride("%ban.duration%", - () => duration?.ToString(@"d\.hh\:mm") ?? "perma") - .Build(); - - // if template isn't set, use the old message style - if (string.IsNullOrWhiteSpace(template)) - { - template = JsonConvert.SerializeObject(new - { - color = _bcs.Data.Color.Error.PackedValue >> 8, - description = defaultMessage - }); - } - // if template is set to "-" do not dm the user - else if (template == "-") - return default; - // if template is an embed, send that embed with replacements - // otherwise, treat template as a regular string with replacements - else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true }) - { - template = JsonConvert.SerializeObject(new - { - color = _bcs.Data.Color.Error.PackedValue >> 8, - description = template - }); - } - - var output = SmartText.CreateFrom(template); - return replacer.Replace(output); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleCommands.cs b/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleCommands.cs deleted file mode 100644 index 9e534ad..0000000 --- a/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleCommands.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable disable -using Ellie.Modules.Administration.Services; - -namespace Ellie.Modules.Administration; - -public partial class Administration -{ - [Group] - public partial class VcRoleCommands : EllieModule - { - [Cmd] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [RequireContext(ContextType.Guild)] - public async Task VcRoleRm(ulong vcId) - { - if (_service.RemoveVcRole(ctx.Guild.Id, vcId)) - await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vcId.ToString()))); - else - await ReplyErrorLocalizedAsync(strs.vcrole_not_found); - } - - [Cmd] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [RequireContext(ContextType.Guild)] - public async Task VcRole([Leftover] IRole role = null) - { - var user = (IGuildUser)ctx.User; - - var vc = user.VoiceChannel; - - if (vc is null || vc.GuildId != user.GuildId) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - if (role is null) - { - if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id)) - await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vc.Name))); - } - else - { - _service.AddVcRole(ctx.Guild.Id, role, vc.Id); - await ReplyConfirmLocalizedAsync(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task VcRoleList() - { - var guild = (SocketGuild)ctx.Guild; - string text; - if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out var roles)) - { - if (!roles.Any()) - text = GetText(strs.no_vcroles); - else - { - text = string.Join("\n", - roles.Select(x - => $"{Format.Bold(guild.GetVoiceChannel(x.Key)?.Name ?? x.Key.ToString())} => {x.Value}")); - } - } - else - text = GetText(strs.no_vcroles); - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.vc_role_list)) - .WithDescription(text)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleService.cs b/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleService.cs deleted file mode 100644 index b9ba4d0..0000000 --- a/src/Ellie.Bot.Modules.Administration/VcRole/VcRoleService.cs +++ /dev/null @@ -1,209 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Administration.Services; - -public class VcRoleService : IEService -{ - public ConcurrentDictionary> VcRoles { get; } - public ConcurrentDictionary> ToAssign { get; } - private readonly DbService _db; - private readonly DiscordSocketClient _client; - - public VcRoleService(DiscordSocketClient client, IBot bot, DbService db) - { - _db = db; - _client = client; - - _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated; - VcRoles = new(); - ToAssign = new(); - - using (var uow = db.GetDbContext()) - { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - uow.Set() - .AsQueryable() - .Include(x => x.VcRoleInfos) - .Where(x => guildIds.Contains(x.GuildId)) - .AsEnumerable() - .Select(InitializeVcRole) - .WhenAll(); - } - - Task.Run(async () => - { - while (true) - { - Task Selector(System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)> queue) - { - return Task.Run(async () => - { - while (queue.TryDequeue(out var item)) - { - var (add, user, role) = item; - - try - { - if (add) - { - if (!user.RoleIds.Contains(role.Id)) - await user.AddRoleAsync(role); - } - else - { - if (user.RoleIds.Contains(role.Id)) - await user.RemoveRoleAsync(role); - } - } - catch - { - } - - await Task.Delay(250); - } - }); - } - - await ToAssign.Values.Select(Selector).Append(Task.Delay(1000)).WhenAll(); - } - }); - - _client.LeftGuild += _client_LeftGuild; - bot.JoinedGuild += Bot_JoinedGuild; - } - - private Task Bot_JoinedGuild(GuildConfig arg) - { - // includeall no longer loads vcrole - // need to load new guildconfig with vc role included - using (var uow = _db.GetDbContext()) - { - var configWithVcRole = uow.GuildConfigsForId(arg.GuildId, set => set.Include(x => x.VcRoleInfos)); - _ = InitializeVcRole(configWithVcRole); - } - - return Task.CompletedTask; - } - - private Task _client_LeftGuild(SocketGuild arg) - { - VcRoles.TryRemove(arg.Id, out _); - ToAssign.TryRemove(arg.Id, out _); - return Task.CompletedTask; - } - - private async Task InitializeVcRole(GuildConfig gconf) - { - var g = _client.GetGuild(gconf.GuildId); - if (g is null) - return; - - var infos = new ConcurrentDictionary(); - var missingRoles = new List(); - VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; }); - foreach (var ri in gconf.VcRoleInfos) - { - var role = g.GetRole(ri.RoleId); - if (role is null) - { - missingRoles.Add(ri); - continue; - } - - infos.TryAdd(ri.VoiceChannelId, role); - } - - if (missingRoles.Any()) - { - await using var uow = _db.GetDbContext(); - uow.RemoveRange(missingRoles); - await uow.SaveChangesAsync(); - - Log.Warning("Removed {MissingRoleCount} missing roles from {ServiceName}", - missingRoles.Count, - nameof(VcRoleService)); - } - } - - public void AddVcRole(ulong guildId, IRole role, ulong vcId) - { - if (role is null) - throw new ArgumentNullException(nameof(role)); - - var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary()); - - guildVcRoles.AddOrUpdate(vcId, role, (_, _) => role); - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); - var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one - if (toDelete is not null) - uow.Remove(toDelete); - conf.VcRoleInfos.Add(new() - { - VoiceChannelId = vcId, - RoleId = role.Id - }); // add new one - uow.SaveChanges(); - } - - public bool RemoveVcRole(ulong guildId, ulong vcId) - { - if (!VcRoles.TryGetValue(guildId, out var guildVcRoles)) - return false; - - if (!guildVcRoles.TryRemove(vcId, out _)) - return false; - - using var uow = _db.GetDbContext(); - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); - var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList(); - uow.RemoveRange(toRemove); - uow.SaveChanges(); - - return true; - } - - private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) - { - if (usr is not SocketGuildUser gusr) - return Task.CompletedTask; - - var oldVc = oldState.VoiceChannel; - var newVc = newState.VoiceChannel; - _ = Task.Run(() => - { - try - { - if (oldVc != newVc) - { - ulong guildId; - guildId = newVc?.Guild.Id ?? oldVc.Guild.Id; - - if (VcRoles.TryGetValue(guildId, out var guildVcRoles)) - { - //remove old - if (oldVc is not null && guildVcRoles.TryGetValue(oldVc.Id, out var role)) - Assign(false, gusr, role); - //add new - if (newVc is not null && guildVcRoles.TryGetValue(newVc.Id, out role)) - Assign(true, gusr, role); - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate"); - } - }); - return Task.CompletedTask; - } - - private void Assign(bool v, SocketGuildUser gusr, IRole role) - { - var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)>()); - queue.Enqueue((v, gusr, role)); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs b/src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs deleted file mode 100644 index a372896..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs +++ /dev/null @@ -1,395 +0,0 @@ -#nullable disable - -using Ellie.Common.Attributes; - -namespace Ellie.Modules.EllieExpressions; - -[Name("Expressions")] -public partial class EllieExpressions : EllieModule -{ - public enum All - { - All - } - - private readonly IBotCredentials _creds; - private readonly IHttpClientFactory _clientFactory; - - public EllieExpressions(IBotCredentials creds, IHttpClientFactory clientFactory) - { - _creds = creds; - _clientFactory = clientFactory; - } - - private bool AdminInGuildOrOwnerInDm() - => (ctx.Guild is null && _creds.IsOwner(ctx.User)) - || (ctx.Guild is not null && ((IGuildUser)ctx.User).GuildPermissions.Administrator); - - private async Task ExprAddInternalAsync(string key, string message) - { - if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) - { - return; - } - - var ex = await _service.AddAsync(ctx.Guild?.Id, key, message); - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.expr_new)) - .WithDescription($"#{new kwum(ex.Id)}") - .AddField(GetText(strs.trigger), key) - .AddField(GetText(strs.response), - message.Length > 1024 ? GetText(strs.redacted_too_long) : message)); - } - - [Cmd] - [UserPerm(GuildPerm.Administrator)] - public async Task ExprToggleGlobal() - { - var result = await _service.ToggleGlobalExpressionsAsync(ctx.Guild.Id); - if (result) - await ReplyConfirmLocalizedAsync(strs.expr_global_disabled); - else - await ReplyConfirmLocalizedAsync(strs.expr_global_enabled); - } - - [Cmd] - [UserPerm(GuildPerm.Administrator)] - public async Task ExprAddServer(string key, [Leftover] string message) - { - if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) - { - return; - } - - await ExprAddInternalAsync(key, message); - } - - [Cmd] - public async Task ExprAdd(string key, [Leftover] string message) - { - if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) - { - return; - } - - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - await ExprAddInternalAsync(key, message); - } - - [Cmd] - public async Task ExprEdit(kwum id, [Leftover] string message) - { - var channel = ctx.Channel as ITextChannel; - if (string.IsNullOrWhiteSpace(message) || id < 0) - { - return; - } - - if ((channel is null && !_creds.IsOwner(ctx.User)) - || (channel is not null && !((IGuildUser)ctx.User).GuildPermissions.Administrator)) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - var ex = await _service.EditAsync(ctx.Guild?.Id, id, message); - if (ex is not null) - { - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.expr_edited)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), ex.Trigger) - .AddField(GetText(strs.response), - message.Length > 1024 ? GetText(strs.redacted_too_long) : message)); - } - else - { - await ReplyErrorLocalizedAsync(strs.expr_no_found_id); - } - } - - [Cmd] - [Priority(1)] - public async Task ExprList(int page = 1) - { - if (--page < 0 || page > 999) - { - return; - } - - var expressions = _service.GetExpressionsFor(ctx.Guild?.Id); - - if (expressions is null || !expressions.Any()) - { - await ReplyErrorLocalizedAsync(strs.expr_no_found); - return; - } - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var desc = expressions.OrderBy(ex => ex.Trigger) - .Skip(curPage * 20) - .Take(20) - .Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}" - + $"{(ex.DmResponse ? "✉" : "◾")}" - + $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}" - + $"`{(kwum)ex.Id}` {ex.Trigger}" - + (string.IsNullOrWhiteSpace(ex.Reactions) - ? string.Empty - : " // " + string.Join(" ", ex.GetReactions()))) - .Join('\n'); - - return _eb.Create().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc); - }, - expressions.Length, - 20); - } - - [Cmd] - public async Task ExprShow(kwum id) - { - var found = _service.GetExpression(ctx.Guild?.Id, id); - - if (found is null) - { - await ReplyErrorLocalizedAsync(strs.expr_no_found_id); - return; - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), - found.Response.TrimTo(1000).Replace("](", "]\\("))); - } - - public async Task ExprDeleteInternalAsync(kwum id) - { - var ex = await _service.DeleteAsync(ctx.Guild?.Id, id); - - if (ex is not null) - { - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.expr_deleted)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), ex.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), ex.Response.TrimTo(1024))); - } - else - { - await ReplyErrorLocalizedAsync(strs.expr_no_found_id); - } - } - - [Cmd] - [UserPerm(GuildPerm.Administrator)] - [RequireContext(ContextType.Guild)] - public async Task ExprDeleteServer(kwum id) - => await ExprDeleteInternalAsync(id); - - [Cmd] - public async Task ExprDelete(kwum id) - { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - await ExprDeleteInternalAsync(id); - } - - [Cmd] - public async Task ExprReact(kwum id, params string[] emojiStrs) - { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - var ex = _service.GetExpression(ctx.Guild?.Id, id); - if (ex is null) - { - await ReplyErrorLocalizedAsync(strs.expr_no_found_id); - return; - } - - if (emojiStrs.Length == 0) - { - await _service.ResetExprReactions(ctx.Guild?.Id, id); - await ReplyConfirmLocalizedAsync(strs.expr_reset(Format.Bold(id.ToString()))); - return; - } - - var succ = new List(); - foreach (var emojiStr in emojiStrs) - { - var emote = emojiStr.ToIEmote(); - - // i should try adding these emojis right away to the message, to make sure the bot can react with these emojis. If it fails, skip that emoji - try - { - await ctx.Message.AddReactionAsync(emote); - await Task.Delay(100); - succ.Add(emojiStr); - - if (succ.Count >= 3) - { - break; - } - } - catch { } - } - - if (succ.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.invalid_emojis); - return; - } - - await _service.SetExprReactions(ctx.Guild?.Id, id, succ); - - - await ReplyConfirmLocalizedAsync(strs.expr_set(Format.Bold(id.ToString()), - succ.Select(static x => x.ToString()).Join(", "))); - } - - [Cmd] - public Task ExprCa(kwum id) - => InternalExprEdit(id, ExprField.ContainsAnywhere); - - [Cmd] - public Task ExprDm(kwum id) - => InternalExprEdit(id, ExprField.DmResponse); - - [Cmd] - public Task ExprAd(kwum id) - => InternalExprEdit(id, ExprField.AutoDelete); - - [Cmd] - public Task ExprAt(kwum id) - => InternalExprEdit(id, ExprField.AllowTarget); - - [Cmd] - [OwnerOnly] - public async Task ExprsReload() - { - await _service.TriggerReloadExpressions(); - - await ctx.OkAsync(); - } - - private async Task InternalExprEdit(kwum id, ExprField option) - { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - var (success, newVal) = await _service.ToggleExprOptionAsync(ctx.Guild?.Id, id, option); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.expr_no_found_id); - return; - } - - if (newVal) - { - await ReplyConfirmLocalizedAsync(strs.option_enabled(Format.Code(option.ToString()), - Format.Code(id.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.option_disabled(Format.Code(option.ToString()), - Format.Code(id.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ExprClear() - { - if (await PromptUserConfirmAsync(_eb.Create() - .WithTitle("Expression clear") - .WithDescription("This will delete all expressions on this server."))) - { - var count = _service.DeleteAllExpressions(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.exprs_cleared(count)); - } - } - - [Cmd] - public async Task ExprsExport() - { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var serialized = _service.ExportExpressions(ctx.Guild?.Id); - await using var stream = await serialized.ToStream(); - await ctx.Channel.SendFileAsync(stream, "exprs-export.yml"); - } - - [Cmd] -#if GLOBAL_ELLIE - [OwnerOnly] -#endif - public async Task ExprsImport([Leftover] string input = null) - { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.expr_insuff_perms); - return; - } - - input = input?.Trim(); - - _ = ctx.Channel.TriggerTypingAsync(); - - if (input is null) - { - var attachment = ctx.Message.Attachments.FirstOrDefault(); - if (attachment is null) - { - await ReplyErrorLocalizedAsync(strs.expr_import_no_input); - return; - } - - using var client = _clientFactory.CreateClient(); - input = await client.GetStringAsync(attachment.Url); - - if (string.IsNullOrWhiteSpace(input)) - { - await ReplyErrorLocalizedAsync(strs.expr_import_no_input); - return; - } - } - - var succ = await _service.ImportExpressionsAsync(ctx.Guild?.Id, input); - if (!succ) - { - await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data); - return; - } - - await ctx.OkAsync(); - } -} diff --git a/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsExtensios.cs b/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsExtensios.cs deleted file mode 100644 index 924ab13..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsExtensios.cs +++ /dev/null @@ -1,88 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; -using System.Runtime.CompilerServices; - -namespace Ellie.Modules.EllieExpressions; - -public static class EllieExpressionExtensions -{ - private static string ResolveTriggerString(this string str, DiscordSocketClient client) - => str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal); - - public static async Task Send( - this EllieExpression cr, - IUserMessage ctx, - DiscordSocketClient client, - bool sanitize) - { - var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel; - - var trigger = cr.Trigger.ResolveTriggerString(client); - var substringIndex = trigger.Length; - if (cr.ContainsAnywhere) - { - var pos = ctx.Content.AsSpan().GetWordPosition(trigger); - if (pos == WordPosition.Start) - substringIndex += 1; - else if (pos == WordPosition.End) - substringIndex = ctx.Content.Length; - else if (pos == WordPosition.Middle) - substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture); - } - - var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true; - - var rep = new ReplacementBuilder() - .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client) - .WithOverride("%target%", - () => canMentionEveryone - ? ctx.Content[substringIndex..].Trim() - : ctx.Content[substringIndex..].Trim().SanitizeMentions(true)) - .Build(); - - var text = SmartText.CreateFrom(cr.Response); - text = rep.Replace(text); - - return await channel.SendAsync(text, sanitize); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static WordPosition GetWordPosition(this ReadOnlySpan str, in ReadOnlySpan word) - { - var wordIndex = str.IndexOf(word, StringComparison.OrdinalIgnoreCase); - if (wordIndex == -1) - return WordPosition.None; - - if (wordIndex == 0) - { - if (word.Length < str.Length && str.IsValidWordDivider(word.Length)) - return WordPosition.Start; - } - else if (wordIndex + word.Length == str.Length) - { - if (str.IsValidWordDivider(wordIndex - 1)) - return WordPosition.End; - } - else if (str.IsValidWordDivider(wordIndex - 1) && str.IsValidWordDivider(wordIndex + word.Length)) - return WordPosition.Middle; - - return WordPosition.None; - } - - private static bool IsValidWordDivider(this in ReadOnlySpan str, int index) - { - var ch = str[index]; - if (ch is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '1' and <= '9') - return false; - - return true; - } -} - -public enum WordPosition -{ - None, - Start, - Middle, - End -} diff --git a/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs b/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs deleted file mode 100644 index 76cde4a..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs +++ /dev/null @@ -1,764 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Common.Yml; -using Ellie.Db; -using Ellie.Services.Database.Models; -using System.Runtime.CompilerServices; -using LinqToDB.EntityFrameworkCore; -using Ellie.Bot.Common; -using Ellie.Services; -using Serilog; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Ellie.Modules.EllieExpressions; - -public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor -{ - private const string MENTION_PH = "%bot.mention%"; - - private const string PREPEND_EXPORT = - """ - # Keys are triggers, Each key has a LIST of expressions in the following format: - # - res: Response string - # id: Alphanumeric id used for commands related to the expression. (Note, when using .exprsimport, a new id will be generated.) - # react: - # - - # at: Whether expression allows targets (see .h .exprat) - # ca: Whether expression expects trigger anywhere (see .h .exprca) - # dm: Whether expression DMs the response (see .h .exprdm) - # ad: Whether expression automatically deletes triggering message (see .h .exprad) - - - """; - - private static readonly ISerializer _exportSerializer = new SerializerBuilder() - .WithEventEmitter(args - => new MultilineScalarFlowStyleEmitter(args)) - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithIndentedSequences() - .ConfigureDefaultValuesHandling(DefaultValuesHandling - .OmitDefaults) - .DisableAliases() - .Build(); - - public int Priority - => 0; - - private readonly object _gexprWriteLock = new(); - - private readonly TypedKey _gexprAddedKey = new("gexpr.added"); - private readonly TypedKey _gexprDeletedkey = new("gexpr.deleted"); - private readonly TypedKey _gexprEditedKey = new("gexpr.edited"); - private readonly TypedKey _exprsReloadedKey = new("exprs.reloaded"); - - // it is perfectly fine to have global expressions as an array - // 1. expressions are almost never added (compared to how many times they are being looped through) - // 2. only need write locks for this as we'll rebuild+replace the array on every edit - // 3. there's never many of them (at most a thousand, usually < 100) - private EllieExpression[] globalExpressions = Array.Empty(); - private ConcurrentDictionary newguildExpressions = new(); - - private readonly DbService _db; - private readonly DiscordSocketClient _client; - // private readonly PermissionService _perms; - // private readonly GlobalPermissionService _gperm; - // private readonly CmdCdService _cmdCds; - private readonly IPermissionChecker _permChecker; - private readonly ICommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly IBot _bot; - private readonly IPubSub _pubSub; - private readonly IEmbedBuilderService _eb; - private readonly Random _rng; - - private bool ready; - private ConcurrentHashSet _disabledGlobalExpressionGuilds; - - public EllieExpressionsService( - DbService db, - IBotStrings strings, - IBot bot, - DiscordSocketClient client, - ICommandHandler cmd, - IPubSub pubSub, - IEmbedBuilderService eb, - IPermissionChecker permChecker) - { - _db = db; - _client = client; - _cmd = cmd; - _strings = strings; - _bot = bot; - _pubSub = pubSub; - _eb = eb; - _permChecker = permChecker; - _rng = new EllieRandom(); - - _pubSub.Sub(_exprsReloadedKey, OnExprsShouldReload); - pubSub.Sub(_gexprAddedKey, OnGexprAdded); - pubSub.Sub(_gexprDeletedkey, OnGexprDeleted); - pubSub.Sub(_gexprEditedKey, OnGexprEdited); - - bot.JoinedGuild += OnJoinedGuild; - _client.LeftGuild += OnLeftGuild; - } - - private async Task ReloadInternal(IReadOnlyList allGuildIds) - { - await using var uow = _db.GetDbContext(); - var guildItems = await uow.Set().AsNoTracking() - .Where(x => allGuildIds.Contains(x.GuildId.Value)) - .ToListAsync(); - - newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value) - .ToDictionary(g => g.Key, - g => g.Select(x => - { - x.Trigger = x.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention); - return x; - }) - .ToArray()) - .ToConcurrent(); - - _disabledGlobalExpressionGuilds = new(await uow.Set() - .Where(x => x.DisableGlobalExpressions) - .Select(x => x.GuildId) - .ToListAsyncLinqToDB()); - - lock (_gexprWriteLock) - { - var globalItems = uow.Set().AsNoTracking() - .Where(x => x.GuildId == null || x.GuildId == 0) - .AsEnumerable() - .Select(x => - { - x.Trigger = x.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention); - return x; - }) - .ToArray(); - - globalExpressions = globalItems; - } - - ready = true; - } - - private EllieExpression TryGetExpression(IUserMessage umsg) - { - if (!ready) - return null; - - if (umsg.Channel is not SocketTextChannel channel) - return null; - - var content = umsg.Content.Trim().ToLowerInvariant(); - - if (newguildExpressions.TryGetValue(channel.Guild.Id, out var expressions) && expressions.Length > 0) - { - var expr = MatchExpressions(content, expressions); - if (expr is not null) - return expr; - } - - if (_disabledGlobalExpressionGuilds.Contains(channel.Guild.Id)) - return null; - - var localGrs = globalExpressions; - - return MatchExpressions(content, localGrs); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private EllieExpression MatchExpressions(in ReadOnlySpan content, EllieExpression[] exprs) - { - var result = new List(1); - for (var i = 0; i < exprs.Length; i++) - { - var expr = exprs[i]; - var trigger = expr.Trigger; - if (content.Length > trigger.Length) - { - // if input is greater than the trigger, it can only work if: - // it has CA enabled - if (expr.ContainsAnywhere) - { - // if ca is enabled, we have to check if it is a word within the content - var wp = content.GetWordPosition(trigger); - - // if it is, then that's valid - if (wp != WordPosition.None) - result.Add(expr); - - // if it's not, then it cant' work under any circumstance, - // because content is greater than the trigger length - // so it can't be equal, and it's not contained as a word - continue; - } - - // if CA is disabled, and expr has AllowTarget, then the - // content has to start with the trigger followed by a space - if (expr.AllowTarget - && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase) - && content[trigger.Length] == ' ') - result.Add(expr); - } - else if (content.Length < expr.Trigger.Length) - { - // if input length is less than trigger length, it means - // that the reaction can never be triggered - } - else - { - // if input length is the same as trigger length - // reaction can only trigger if the strings are equal - if (content.SequenceEqual(expr.Trigger)) - result.Add(expr); - } - } - - if (result.Count == 0) - return null; - - var cancelled = result.FirstOrDefault(x => x.Response == "-"); - if (cancelled is not null) - return cancelled; - - return result[_rng.Next(0, result.Count)]; - } - - public async Task ExecOnMessageAsync(IGuild guild, IUserMessage msg) - { - // maybe this message is an expression - var expr = TryGetExpression(msg); - - if (expr is null || expr.Response == "-") - return false; - - var result = await _permChecker.CheckAsync( - guild, - msg.Channel, - msg.Author, - "ACTUALEXPRESSIONS", - expr.Trigger - ); - - if (!result.IsT0) - return false; - - // todo print error etc - - // if (await _cmdCds.TryBlock(guild, msg.Author, expr.Trigger)) - // return false; - - try - { - // if (_gperm.BlockedModules.Contains("ACTUALEXPRESSIONS")) - // { - // Log.Information( - // "User {UserName} [{UserId}] tried to use an expression but 'ActualExpressions' are globally disabled", - // msg.Author.ToString(), - // msg.Author.Id); - // - // return true; - // } - // - // if (guild is SocketGuild sg) - // { - // var pc = _perms.GetCacheFor(guild.Id); - // if (!pc.Permissions.CheckPermissions(msg, expr.Trigger, "ACTUALEXPRESSIONS", out var index)) - // { - // if (pc.Verbose) - // { - // var permissionMessage = _strings.GetText(strs.perm_prevent(index + 1, - // Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg))), - // sg.Id); - // - // try - // { - // await msg.Channel.SendErrorAsync(_eb, permissionMessage); - // } - // catch - // { - // } - // - // Log.Information("{PermissionMessage}", permissionMessage); - // } - // - // return true; - // } - // } - - var sentMsg = await expr.Send(msg, _client, false); - - var reactions = expr.GetReactions(); - foreach (var reaction in reactions) - { - try - { - await sentMsg.AddReactionAsync(reaction.ToIEmote()); - } - catch - { - Log.Warning("Unable to add reactions to message {Message} in server {GuildId}", - sentMsg.Id, - expr.GuildId); - break; - } - - await Task.Delay(1000); - } - - if (expr.AutoDeleteTrigger) - { - try - { - await msg.DeleteAsync(); - } - catch - { - } - } - - Log.Information("s: {GuildId} c: {ChannelId} u: {UserId} | {UserName} executed expression {Expr}", - guild.Id, - msg.Channel.Id, - msg.Author.Id, - msg.Author.ToString(), - expr.Trigger); - - return true; - } - catch (Exception ex) - { - Log.Warning(ex, "Error in Expression RunBehavior: {ErrorMessage}", ex.Message); - } - - return false; - } - - public async Task ResetExprReactions(ulong? maybeGuildId, int id) - { - EllieExpression expr; - await using var uow = _db.GetDbContext(); - expr = uow.Set().GetById(id); - if (expr is null) - return; - - expr.Reactions = string.Empty; - - await uow.SaveChangesAsync(); - } - - private Task UpdateInternalAsync(ulong? maybeGuildId, EllieExpression expr) - { - if (maybeGuildId is { } guildId) - UpdateInternal(guildId, expr); - else - return _pubSub.Pub(_gexprEditedKey, expr); - - return Task.CompletedTask; - } - - private void UpdateInternal(ulong? maybeGuildId, EllieExpression expr) - { - if (maybeGuildId is { } guildId) - { - newguildExpressions.AddOrUpdate(guildId, - new[] { expr }, - (_, old) => - { - var newArray = old.ToArray(); - for (var i = 0; i < newArray.Length; i++) - { - if (newArray[i].Id == expr.Id) - newArray[i] = expr; - } - - return newArray; - }); - } - else - { - lock (_gexprWriteLock) - { - var exprs = globalExpressions; - for (var i = 0; i < exprs.Length; i++) - { - if (exprs[i].Id == expr.Id) - exprs[i] = expr; - } - } - } - } - - private Task AddInternalAsync(ulong? maybeGuildId, EllieExpression expr) - { - // only do this for perf purposes - expr.Trigger = expr.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention); - - if (maybeGuildId is { } guildId) - newguildExpressions.AddOrUpdate(guildId, new[] { expr }, (_, old) => old.With(expr)); - else - return _pubSub.Pub(_gexprAddedKey, expr); - - return Task.CompletedTask; - } - - private Task DeleteInternalAsync(ulong? maybeGuildId, int id) - { - if (maybeGuildId is { } guildId) - { - newguildExpressions.AddOrUpdate(guildId, - Array.Empty(), - (key, old) => DeleteInternal(old, id, out _)); - - return Task.CompletedTask; - } - - lock (_gexprWriteLock) - { - var expr = Array.Find(globalExpressions, item => item.Id == id); - if (expr is not null) - return _pubSub.Pub(_gexprDeletedkey, expr.Id); - } - - return Task.CompletedTask; - } - - private EllieExpression[] DeleteInternal( - IReadOnlyList exprs, - int id, - out EllieExpression deleted) - { - deleted = null; - if (exprs is null || exprs.Count == 0) - return exprs as EllieExpression[] ?? exprs?.ToArray(); - - var newExprs = new EllieExpression[exprs.Count - 1]; - for (int i = 0, k = 0; i < exprs.Count; i++, k++) - { - if (exprs[i].Id == id) - { - deleted = exprs[i]; - k--; - continue; - } - - newExprs[k] = exprs[i]; - } - - return newExprs; - } - - public async Task SetExprReactions(ulong? guildId, int id, IEnumerable emojis) - { - EllieExpression expr; - await using (var uow = _db.GetDbContext()) - { - expr = uow.Set().GetById(id); - if (expr is null) - return; - - expr.Reactions = string.Join("@@@", emojis); - - await uow.SaveChangesAsync(); - } - - await UpdateInternalAsync(guildId, expr); - } - - public async Task<(bool Sucess, bool NewValue)> ToggleExprOptionAsync(ulong? guildId, int id, ExprField field) - { - var newVal = false; - EllieExpression expr; - await using (var uow = _db.GetDbContext()) - { - expr = uow.Set().GetById(id); - - if (expr is null || expr.GuildId != guildId) - return (false, false); - if (field == ExprField.AutoDelete) - newVal = expr.AutoDeleteTrigger = !expr.AutoDeleteTrigger; - else if (field == ExprField.ContainsAnywhere) - newVal = expr.ContainsAnywhere = !expr.ContainsAnywhere; - else if (field == ExprField.DmResponse) - newVal = expr.DmResponse = !expr.DmResponse; - else if (field == ExprField.AllowTarget) - newVal = expr.AllowTarget = !expr.AllowTarget; - - await uow.SaveChangesAsync(); - } - - await UpdateInternalAsync(guildId, expr); - - return (true, newVal); - } - - public EllieExpression GetExpression(ulong? guildId, int id) - { - using var uow = _db.GetDbContext(); - var expr = uow.Set().GetById(id); - if (expr is null || expr.GuildId != guildId) - return null; - - return expr; - } - - public int DeleteAllExpressions(ulong guildId) - { - using var uow = _db.GetDbContext(); - var count = uow.Set().ClearFromGuild(guildId); - uow.SaveChanges(); - - newguildExpressions.TryRemove(guildId, out _); - - return count; - } - - public bool ExpressionExists(ulong? guildId, string input) - { - input = input.ToLowerInvariant(); - - var gexprs = globalExpressions; - foreach (var t in gexprs) - { - if (t.Trigger == input) - return true; - } - - if (guildId is ulong gid && newguildExpressions.TryGetValue(gid, out var guildExprs)) - { - foreach (var t in guildExprs) - { - if (t.Trigger == input) - return true; - } - } - - return false; - } - - public string ExportExpressions(ulong? guildId) - { - var exprs = GetExpressionsFor(guildId); - - var exprsDict = exprs.GroupBy(x => x.Trigger).ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel)); - - return PREPEND_EXPORT + _exportSerializer.Serialize(exprsDict).UnescapeUnicodeCodePoints(); - } - - public async Task ImportExpressionsAsync(ulong? guildId, string input) - { - Dictionary> data; - try - { - data = Yaml.Deserializer.Deserialize>>(input); - if (data.Sum(x => x.Value.Count) == 0) - return false; - } - catch - { - return false; - } - - await using var uow = _db.GetDbContext(); - foreach (var entry in data) - { - var trigger = entry.Key; - await uow.Set().AddRangeAsync(entry.Value.Where(expr => !string.IsNullOrWhiteSpace(expr.Res)) - .Select(expr => new EllieExpression - { - GuildId = guildId, - Response = expr.Res, - Reactions = expr.React?.Join("@@@"), - Trigger = trigger, - AllowTarget = expr.At, - ContainsAnywhere = expr.Ca, - DmResponse = expr.Dm, - AutoDeleteTrigger = expr.Ad - })); - } - - await uow.SaveChangesAsync(); - await TriggerReloadExpressions(); - return true; - } - - #region Event Handlers - - public async Task OnReadyAsync() - => await OnExprsShouldReload(true); - - private ValueTask OnExprsShouldReload(bool _) - => new(ReloadInternal(_bot.GetCurrentGuildIds())); - - private ValueTask OnGexprAdded(EllieExpression c) - { - lock (_gexprWriteLock) - { - var newGlobalReactions = new EllieExpression[globalExpressions.Length + 1]; - Array.Copy(globalExpressions, newGlobalReactions, globalExpressions.Length); - newGlobalReactions[globalExpressions.Length] = c; - globalExpressions = newGlobalReactions; - } - - return default; - } - - private ValueTask OnGexprEdited(EllieExpression c) - { - lock (_gexprWriteLock) - { - for (var i = 0; i < globalExpressions.Length; i++) - { - if (globalExpressions[i].Id == c.Id) - { - globalExpressions[i] = c; - return default; - } - } - - // if edited expr is not found?! - // add it - OnGexprAdded(c); - } - - return default; - } - - private ValueTask OnGexprDeleted(int id) - { - lock (_gexprWriteLock) - { - var newGlobalReactions = DeleteInternal(globalExpressions, id, out _); - globalExpressions = newGlobalReactions; - } - - return default; - } - - public Task TriggerReloadExpressions() - => _pubSub.Pub(_exprsReloadedKey, true); - - #endregion - - #region Client Event Handlers - - private Task OnLeftGuild(SocketGuild arg) - { - newguildExpressions.TryRemove(arg.Id, out _); - - return Task.CompletedTask; - } - - private async Task OnJoinedGuild(GuildConfig gc) - { - await using var uow = _db.GetDbContext(); - var exprs = await uow.Set().AsNoTracking().Where(x => x.GuildId == gc.GuildId).ToArrayAsync(); - - newguildExpressions[gc.GuildId] = exprs; - } - - #endregion - - #region Basic Operations - - public async Task AddAsync(ulong? guildId, string key, string message) - { - key = key.ToLowerInvariant(); - var expr = new EllieExpression - { - GuildId = guildId, - Trigger = key, - Response = message - }; - - if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - expr.AllowTarget = true; - - await using (var uow = _db.GetDbContext()) - { - uow.Set().Add(expr); - await uow.SaveChangesAsync(); - } - - await AddInternalAsync(guildId, expr); - - return expr; - } - - public async Task EditAsync(ulong? guildId, int id, string message) - { - await using var uow = _db.GetDbContext(); - var expr = uow.Set().GetById(id); - - if (expr is null || expr.GuildId != guildId) - return null; - - // disable allowtarget if message had target, but it was removed from it - if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase) - && expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - expr.AllowTarget = false; - - expr.Response = message; - - // enable allow target if message is edited to contain target - if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - expr.AllowTarget = true; - - await uow.SaveChangesAsync(); - await UpdateInternalAsync(guildId, expr); - - return expr; - } - - - public async Task DeleteAsync(ulong? guildId, int id) - { - await using var uow = _db.GetDbContext(); - var toDelete = uow.Set().GetById(id); - - if (toDelete is null) - return null; - - if ((toDelete.IsGlobal() && guildId is null) || guildId == toDelete.GuildId) - { - uow.Set().Remove(toDelete); - await uow.SaveChangesAsync(); - await DeleteInternalAsync(guildId, id); - return toDelete; - } - - return null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public EllieExpression[] GetExpressionsFor(ulong? maybeGuildId) - { - if (maybeGuildId is { } guildId) - return newguildExpressions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty(); - - return globalExpressions; - } - - #endregion - - public async Task ToggleGlobalExpressionsAsync(ulong guildId) - { - await using var ctx = _db.GetDbContext(); - var gc = ctx.GuildConfigsForId(guildId, set => set); - var toReturn = gc.DisableGlobalExpressions = !gc.DisableGlobalExpressions; - await ctx.SaveChangesAsync(); - - if (toReturn) - _disabledGlobalExpressionGuilds.Add(guildId); - else - _disabledGlobalExpressionGuilds.TryRemove(guildId); - - return toReturn; - } -} diff --git a/src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs b/src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs deleted file mode 100644 index 4533d29..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.EllieExpressions; - -public class ExportedExpr -{ - public string Res { get; set; } - public string Id { get; set; } - public bool Ad { get; set; } - public bool Dm { get; set; } - public bool At { get; set; } - public bool Ca { get; set; } - public string[] React; - - public static ExportedExpr FromModel(EllieExpression cr) - => new() - { - Res = cr.Response, - Id = ((kwum)cr.Id).ToString(), - Ad = cr.AutoDeleteTrigger, - At = cr.AllowTarget, - Ca = cr.ContainsAnywhere, - Dm = cr.DmResponse, - React = string.IsNullOrWhiteSpace(cr.Reactions) ? null : cr.GetReactions() - }; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Expresssions/ExprField.cs b/src/Ellie.Bot.Modules.Expresssions/ExprField.cs deleted file mode 100644 index 1216cfa..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/ExprField.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ellie.Modules.EllieExpressions; - -public enum ExprField -{ - AutoDelete, - DmResponse, - AllowTarget, - ContainsAnywhere, - Message -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Expresssions/TypeReaders/CommandOrExprTypeReader.cs b/src/Ellie.Bot.Modules.Expresssions/TypeReaders/CommandOrExprTypeReader.cs deleted file mode 100644 index bbedcfb..0000000 --- a/src/Ellie.Bot.Modules.Expresssions/TypeReaders/CommandOrExprTypeReader.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -using Ellie.Modules.EllieExpressions; -using Ellie.Services; - -namespace Ellie.Common.TypeReaders; - -public sealed class CommandOrExprTypeReader : EllieTypeReader -{ - private readonly CommandService _cmds; - private readonly ICommandHandler _commandHandler; - private readonly EllieExpressionsService _exprs; - - public CommandOrExprTypeReader(CommandService cmds, EllieExpressionsService exprs, ICommandHandler commandHandler) - { - _cmds = cmds; - _commandHandler = commandHandler; - _exprs = exprs; - } - - public override async ValueTask> ReadAsync(ICommandContext ctx, string input) - { - if (_exprs.ExpressionExists(ctx.Guild?.Id, input)) - return TypeReaderResult.FromSuccess(new CommandOrExprInfo(input, CommandOrExprInfo.Type.Custom)); - - var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input); - if (cmd.IsSuccess) - { - return TypeReaderResult.FromSuccess(new CommandOrExprInfo(((CommandInfo)cmd.Values.First().Value).Name, - CommandOrExprInfo.Type.Normal)); - } - - return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or expression found."); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRace.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRace.cs deleted file mode 100644 index 4a35236..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRace.cs +++ /dev/null @@ -1,154 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; -using Ellie.Modules.Games.Common; - -namespace Ellie.Modules.Gambling.Common.AnimalRacing; - -public sealed class AnimalRace : IDisposable -{ - public enum Phase - { - WaitingForPlayers, - Running, - Ended - } - - public event Func OnStarted = delegate { return Task.CompletedTask; }; - public event Func OnStartingFailed = delegate { return Task.CompletedTask; }; - public event Func OnStateUpdate = delegate { return Task.CompletedTask; }; - public event Func OnEnded = delegate { return Task.CompletedTask; }; - - public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers; - - public IReadOnlyCollection Users - => _users.ToList(); - - public List FinishedUsers { get; } = new(); - public int MaxUsers { get; } - - private readonly SemaphoreSlim _locker = new(1, 1); - private readonly HashSet _users = new(); - private readonly ICurrencyService _currency; - private readonly RaceOptions _options; - private readonly Queue _animalsQueue; - - public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable availableAnimals) - { - _currency = currency; - _options = options; - _animalsQueue = new(availableAnimals); - MaxUsers = _animalsQueue.Count; - - if (_animalsQueue.Count == 0) - CurrentPhase = Phase.Ended; - } - - public void Initialize() //lame name - => _ = Task.Run(async () => - { - await Task.Delay(_options.StartTime * 1000); - - await _locker.WaitAsync(); - try - { - if (CurrentPhase != Phase.WaitingForPlayers) - return; - - await Start(); - } - finally { _locker.Release(); } - }); - - public async Task JoinRace(ulong userId, string userName, long bet = 0) - { - if (bet < 0) - throw new ArgumentOutOfRangeException(nameof(bet)); - - var user = new AnimalRacingUser(userName, userId, bet); - - await _locker.WaitAsync(); - try - { - if (_users.Count == MaxUsers) - throw new AnimalRaceFullException(); - - if (CurrentPhase != Phase.WaitingForPlayers) - throw new AlreadyStartedException(); - - if (!await _currency.RemoveAsync(userId, bet, new("animalrace", "bet"))) - throw new NotEnoughFundsException(); - - if (_users.Contains(user)) - throw new AlreadyJoinedException(); - - var animal = _animalsQueue.Dequeue(); - user.Animal = animal; - _users.Add(user); - - if (_animalsQueue.Count == 0) //start if no more spots left - await Start(); - - return user; - } - finally { _locker.Release(); } - } - - private async Task Start() - { - CurrentPhase = Phase.Running; - if (_users.Count <= 1) - { - foreach (var user in _users) - { - if (user.Bet > 0) - await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund")); - } - - _ = OnStartingFailed?.Invoke(this); - CurrentPhase = Phase.Ended; - return; - } - - _ = OnStarted?.Invoke(this); - _ = Task.Run(async () => - { - var rng = new EllieRandom(); - while (!_users.All(x => x.Progress >= 60)) - { - foreach (var user in _users) - { - user.Progress += rng.Next(1, 11); - if (user.Progress >= 60) - user.Progress = 60; - } - - var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)).Shuffle(); - - FinishedUsers.AddRange(finished); - - _ = OnStateUpdate?.Invoke(this); - await Task.Delay(2500); - } - - if (FinishedUsers[0].Bet > 0) - { - await _currency.AddAsync(FinishedUsers[0].UserId, - FinishedUsers[0].Bet * (_users.Count - 1), - new("animalrace", "win")); - } - - _ = OnEnded?.Invoke(this); - }); - } - - public void Dispose() - { - CurrentPhase = Phase.Ended; - OnStarted = null; - OnEnded = null; - OnStartingFailed = null; - OnStateUpdate = null; - _locker.Dispose(); - _users.Clear(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRaceService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRaceService.cs deleted file mode 100644 index 4a73781..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRaceService.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common.AnimalRacing; - -namespace Ellie.Modules.Gambling.Services; - -public class AnimalRaceService : IEService -{ - public ConcurrentDictionary AnimalRaces { get; } = new(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingCommands.cs deleted file mode 100644 index 3de1171..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingCommands.cs +++ /dev/null @@ -1,183 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.AnimalRacing; -using Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; -using Ellie.Modules.Gambling.Services; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Gambling; - -// wth is this, needs full rewrite -public partial class Gambling -{ - [Group] - public partial class AnimalRacingCommands : GamblingSubmodule - { - private readonly ICurrencyService _cs; - private readonly DiscordSocketClient _client; - private readonly GamesConfigService _gamesConf; - - private IUserMessage raceMessage; - - public AnimalRacingCommands( - ICurrencyService cs, - DiscordSocketClient client, - GamblingConfigService gamblingConf, - GamesConfigService gamesConf) - : base(gamblingConf) - { - _cs = cs; - _client = client; - _gamesConf = gamesConf; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public Task Race(params string[] args) - { - var (options, _) = OptionsParser.ParseFrom(new RaceOptions(), args); - - var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle()); - if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar)) - return SendErrorAsync(GetText(strs.animal_race), GetText(strs.animal_race_already_started)); - - ar.Initialize(); - - var count = 0; - - Task ClientMessageReceived(SocketMessage arg) - { - _ = Task.Run(() => - { - try - { - if (arg.Channel.Id == ctx.Channel.Id) - { - if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0) - raceMessage = null; - } - } - catch { } - }); - return Task.CompletedTask; - } - - Task ArOnEnded(AnimalRace race) - { - _client.MessageReceived -= ClientMessageReceived; - _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); - var winner = race.FinishedUsers[0]; - if (race.FinishedUsers[0].Bet > 0) - { - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_won_money(Format.Bold(winner.Username), - winner.Animal.Icon, - (race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign))); - } - - ar.Dispose(); - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon))); - } - - ar.OnStartingFailed += Ar_OnStartingFailed; - ar.OnStateUpdate += Ar_OnStateUpdate; - ar.OnEnded += ArOnEnded; - ar.OnStarted += Ar_OnStarted; - _client.MessageReceived += ClientMessageReceived; - - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_starting(options.StartTime)), - footer: GetText(strs.animal_race_join_instr(prefix))); - } - - private Task Ar_OnStarted(AnimalRace race) - { - if (race.Users.Count == race.MaxUsers) - return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)); - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_starting_with_x(race.Users.Count))); - } - - private async Task Ar_OnStateUpdate(AnimalRace race) - { - var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| -{string.Join("\n", race.Users.Select(p => -{ - var index = race.FinishedUsers.IndexOf(p); - var extra = index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}"; - return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}"; -}))} -|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|"; - - var msg = raceMessage; - - if (msg is null) - raceMessage = await SendConfirmAsync(text); - else - { - await msg.ModifyAsync(x => x.Embed = _eb.Create() - .WithTitle(GetText(strs.animal_race)) - .WithDescription(text) - .WithOkColor() - .Build()); - } - } - - private Task Ar_OnStartingFailed(AnimalRace race) - { - _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); - race.Dispose(); - return ReplyErrorLocalizedAsync(strs.animal_race_failed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task JoinRace([OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default) - { - if (!await CheckBetOptional(amount)) - return; - - if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar)) - { - await ReplyErrorLocalizedAsync(strs.race_not_exist); - return; - } - - try - { - var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount); - if (amount > 0) - { - await SendConfirmAsync(GetText(strs.animal_race_join_bet(ctx.User.Mention, - user.Animal.Icon, - amount + CurrencySign))); - } - else - await SendConfirmAsync(GetText(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon))); - } - catch (ArgumentOutOfRangeException) - { - //ignore if user inputed an invalid amount - } - catch (AlreadyJoinedException) - { - // just ignore this - } - catch (AlreadyStartedException) - { - //ignore - } - catch (AnimalRaceFullException) - { - await SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)); - } - catch (NotEnoughFundsException) - { - await SendErrorAsync(GetText(strs.not_enough(CurrencySign))); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingUser.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingUser.cs deleted file mode 100644 index 66bfd48..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/AnimalRacingUser.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common; - -namespace Ellie.Modules.Gambling.Common.AnimalRacing; - -public class AnimalRacingUser -{ - public long Bet { get; } - public string Username { get; } - public ulong UserId { get; } - public RaceAnimal Animal { get; set; } - public int Progress { get; set; } - - public AnimalRacingUser(string username, ulong userId, long bet) - { - Bet = bet; - Username = username; - UserId = userId; - } - - public override bool Equals(object obj) - => obj is AnimalRacingUser x ? x.UserId == UserId : false; - - public override int GetHashCode() - => UserId.GetHashCode(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyJoinedException.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyJoinedException.cs deleted file mode 100644 index 9099331..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyJoinedException.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; - -public class AlreadyJoinedException : Exception -{ - public AlreadyJoinedException() - { - } - - public AlreadyJoinedException(string message) - : base(message) - { - } - - public AlreadyJoinedException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyStartedException.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyStartedException.cs deleted file mode 100644 index 70e08ba..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AlreadyStartedException.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; - -public class AlreadyStartedException : Exception -{ - public AlreadyStartedException() - { - } - - public AlreadyStartedException(string message) - : base(message) - { - } - - public AlreadyStartedException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AnimalRaceFullException.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AnimalRaceFullException.cs deleted file mode 100644 index 8a32104..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/AnimalRaceFullException.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; - -public class AnimalRaceFullException : Exception -{ - public AnimalRaceFullException() - { - } - - public AnimalRaceFullException(string message) - : base(message) - { - } - - public AnimalRaceFullException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/NotEnoughFundsException.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/NotEnoughFundsException.cs deleted file mode 100644 index 7f3d70d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/Exceptions/NotEnoughFundsException.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.AnimalRacing.Exceptions; - -public class NotEnoughFundsException : Exception -{ - public NotEnoughFundsException() - { - } - - public NotEnoughFundsException(string message) - : base(message) - { - } - - public NotEnoughFundsException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/RaceOptions.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/RaceOptions.cs deleted file mode 100644 index 21e8c3a..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/AnimalRacing/RaceOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -using CommandLine; - -namespace Ellie.Modules.Gambling.Common.AnimalRacing; - -public class RaceOptions : IEllieCommandOptions -{ - [Option('s', "start-time", Default = 20, Required = false)] - public int StartTime { get; set; } = 20; - - public void NormalizeOptions() - { - if (StartTime is < 10 or > 120) - StartTime = 20; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankCommands.cs deleted file mode 100644 index 8c67afb..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankCommands.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Bank; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Name("Bank")] - [Group("bank")] - public partial class BankCommands : GamblingModule - { - private readonly IBankService _bank; - private readonly DiscordSocketClient _client; - - public BankCommands(GamblingConfigService gcs, - IBankService bank, - DiscordSocketClient client) : base(gcs) - { - _bank = bank; - _client = client; - } - - [Cmd] - public async Task BankDeposit([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - { - if (amount <= 0) - return; - - if (await _bank.DepositAsync(ctx.User.Id, amount)) - { - await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount))); - } - else - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - } - - [Cmd] - public async Task BankWithdraw([OverrideTypeReader(typeof(BankBalanceTypeReader))] long amount) - { - if (amount <= 0) - return; - - if (await _bank.WithdrawAsync(ctx.User.Id, amount)) - { - await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount))); - } - else - { - await ReplyErrorLocalizedAsync(strs.bank_withdraw_insuff(CurrencySign)); - } - } - - [Cmd] - public async Task BankBalance() - { - var bal = await _bank.GetBalanceAsync(ctx.User.Id); - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithDescription(GetText(strs.bank_balance(N(bal)))); - - try - { - await ctx.User.EmbedAsync(eb); - await ctx.OkAsync(); - } - catch - { - await ReplyErrorLocalizedAsync(strs.cant_dm); - } - } - - private async Task BankTakeInternalAsync(long amount, ulong userId) - { - if (await _bank.TakeAsync(userId, amount)) - { - await ctx.OkAsync(); - return; - } - - await ReplyErrorLocalizedAsync(strs.take_fail(N(amount), - _client.GetUser(userId)?.ToString() - ?? userId.ToString(), - CurrencySign)); - } - - private async Task BankAwardInternalAsync(long amount, ulong userId) - { - if (await _bank.AwardAsync(userId, amount)) - { - await ctx.OkAsync(); - return; - } - - } - - [Cmd] - [OwnerOnly] - [Priority(1)] - public async Task BankTake(long amount, [Leftover] IUser user) - => await BankTakeInternalAsync(amount, user.Id); - - [Cmd] - [OwnerOnly] - [Priority(0)] - public async Task BankTake(long amount, ulong userId) - => await BankTakeInternalAsync(amount, userId); - - [Cmd] - [OwnerOnly] - public async Task BankAward(long amount, [Leftover] IUser user) - => await BankAwardInternalAsync(amount, user.Id); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankService.cs deleted file mode 100644 index dba4b0b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Bank/BankService.cs +++ /dev/null @@ -1,119 +0,0 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Db.Models; - -namespace Ellie.Modules.Gambling.Bank; - -public sealed class BankService : IBankService, IEService -{ - private readonly ICurrencyService _cur; - private readonly DbService _db; - - public BankService(ICurrencyService cur, DbService db) - { - _cur = cur; - _db = db; - } - - public async Task AwardAsync(ulong userId, long amount) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - await using var ctx = _db.GetDbContext(); - await ctx.GetTable() - .InsertOrUpdateAsync(() => new() - { - UserId = userId, - Balance = amount - }, - (old) => new() - { - Balance = old.Balance + amount - }, - () => new() - { - UserId = userId - }); - - return true; - } - - public async Task TakeAsync(ulong userId, long amount) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - await using var ctx = _db.GetDbContext(); - var rows = await ctx.Set() - .ToLinqToDBTable() - .Where(x => x.UserId == userId && x.Balance >= amount) - .UpdateAsync((old) => new() - { - Balance = old.Balance - amount - }); - - return rows > 0; - } - - public async Task DepositAsync(ulong userId, long amount) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (!await _cur.RemoveAsync(userId, amount, new("bank", "deposit"))) - return false; - - await using var ctx = _db.GetDbContext(); - await ctx.Set() - .ToLinqToDBTable() - .InsertOrUpdateAsync(() => new() - { - UserId = userId, - Balance = amount - }, - (old) => new() - { - Balance = old.Balance + amount - }, - () => new() - { - UserId = userId - }); - - return true; - } - - public async Task WithdrawAsync(ulong userId, long amount) - { - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - await using var ctx = _db.GetDbContext(); - var rows = await ctx.Set() - .ToLinqToDBTable() - .Where(x => x.UserId == userId && x.Balance >= amount) - .UpdateAsync((old) => new() - { - Balance = old.Balance - amount - }); - - if (rows > 0) - { - await _cur.AddAsync(userId, amount, new("bank", "withdraw")); - return true; - } - - return false; - } - - public async Task GetBalanceAsync(ulong userId) - { - await using var ctx = _db.GetDbContext(); - return (await ctx.Set() - .ToLinqToDBTable() - .FirstOrDefaultAsync(x => x.UserId == userId)) - ?.Balance - ?? 0; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackCommands.cs deleted file mode 100644 index 246eba9..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackCommands.cs +++ /dev/null @@ -1,183 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Blackjack; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - public partial class BlackJackCommands : GamblingSubmodule - { - public enum BjAction - { - Hit = int.MinValue, - Stand, - Double - } - - private readonly ICurrencyService _cs; - private readonly DbService _db; - private IUserMessage msg; - - public BlackJackCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConf) - : base(gamblingConf) - { - _cs = cs; - _db = db; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task BlackJack([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - { - if (!await CheckBetMandatory(amount)) - return; - - var newBj = new Blackjack(_cs); - Blackjack bj; - if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj))) - { - if (!await bj.Join(ctx.User, amount)) - { - _service.Games.TryRemove(ctx.Channel.Id, out _); - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - bj.StateUpdated += Bj_StateUpdated; - bj.GameEnded += Bj_GameEnded; - bj.Start(); - - await ReplyConfirmLocalizedAsync(strs.bj_created); - } - else - { - if (await bj.Join(ctx.User, amount)) - await ReplyConfirmLocalizedAsync(strs.bj_joined); - else - { - Log.Information("{User} can't join a blackjack game as it's in {BlackjackState} state already", - ctx.User, - bj.State); - } - } - - await ctx.Message.DeleteAsync(); - } - - private Task Bj_GameEnded(Blackjack arg) - { - _service.Games.TryRemove(ctx.Channel.Id, out _); - return Task.CompletedTask; - } - - private async Task Bj_StateUpdated(Blackjack bj) - { - try - { - if (msg is not null) - _ = msg.DeleteAsync(); - - var c = bj.Dealer.Cards.Select(x => x.GetEmojiString()) - .ToList(); - var dealerIcon = "❔ "; - if (bj.State == Blackjack.GameState.Ended) - { - if (bj.Dealer.GetHandValue() == 21) - dealerIcon = "💰 "; - else if (bj.Dealer.GetHandValue() > 21) - dealerIcon = "💥 "; - else - dealerIcon = "🏁 "; - } - - var cStr = string.Concat(c.Select(x => x[..^1] + " ")); - cStr += "\n" + string.Concat(c.Select(x => x.Last() + " ")); - var embed = _eb.Create() - .WithOkColor() - .WithTitle("BlackJack") - .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr); - - if (bj.CurrentUser is not null) - embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser}"); - - foreach (var p in bj.Players) - { - c = p.Cards.Select(x => x.GetEmojiString()).ToList(); - cStr = "-\t" + string.Concat(c.Select(x => x[..^1] + " ")); - cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " ")); - var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {N(p.Bet)} | Value: {p.GetHandValue()}"; - if (bj.State == Blackjack.GameState.Ended) - { - if (p.State == User.UserState.Lost) - full = "❌ " + full; - else - full = "✅ " + full; - } - else if (p == bj.CurrentUser) - full = "▶ " + full; - else if (p.State == User.UserState.Stand) - full = "⏹ " + full; - else if (p.State == User.UserState.Bust) - full = "💥 " + full; - else if (p.State == User.UserState.Blackjack) - full = "💰 " + full; - - embed.AddField(full, cStr); - } - - msg = await ctx.Channel.EmbedAsync(embed); - } - catch - { - } - } - - private string UserToString(User x) - { - var playerName = x.State == User.UserState.Bust - ? Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30)) - : x.DiscordUser.ToString(); - - // var hand = $"{string.Concat(x.Cards.Select(y => "〖" + y.GetEmojiString() + "〗"))}"; - - - return $"{playerName} | Bet: {x.Bet}\n"; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Hit() - => InternalBlackJack(BjAction.Hit); - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Stand() - => InternalBlackJack(BjAction.Stand); - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Double() - => InternalBlackJack(BjAction.Double); - - private async Task InternalBlackJack(BjAction a) - { - if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj)) - return; - - if (a == BjAction.Hit) - await bj.Hit(ctx.User); - else if (a == BjAction.Stand) - await bj.Stand(ctx.User); - else if (a == BjAction.Double) - { - if (!await bj.Double(ctx.User)) - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - - await ctx.Message.DeleteAsync(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackService.cs deleted file mode 100644 index 216cc2f..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/BlackJackService.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common.Blackjack; - -namespace Ellie.Modules.Gambling.Services; - -public class BlackJackService : IEService -{ - public ConcurrentDictionary Games { get; } = new(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Blackjack.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Blackjack.cs deleted file mode 100644 index 95f545a..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Blackjack.cs +++ /dev/null @@ -1,329 +0,0 @@ -#nullable disable -using Ellie.Econ; - -namespace Ellie.Modules.Gambling.Common.Blackjack; - -public class Blackjack -{ - public enum GameState - { - Starting, - Playing, - Ended - } - - public event Func StateUpdated; - public event Func GameEnded; - - private Deck Deck { get; } = new QuadDeck(); - public Dealer Dealer { get; set; } - - - public List Players { get; set; } = new(); - public GameState State { get; set; } = GameState.Starting; - public User CurrentUser { get; private set; } - - private TaskCompletionSource currentUserMove; - private readonly ICurrencyService _cs; - - private readonly SemaphoreSlim _locker = new(1, 1); - - public Blackjack(ICurrencyService cs) - { - _cs = cs; - Dealer = new(); - } - - public void Start() - => _ = GameLoop(); - - public async Task GameLoop() - { - try - { - //wait for players to join - await Task.Delay(20000); - await _locker.WaitAsync(); - try - { - State = GameState.Playing; - } - finally - { - _locker.Release(); - } - - await PrintState(); - //if no users joined the game, end it - if (!Players.Any()) - { - State = GameState.Ended; - _ = GameEnded?.Invoke(this); - return; - } - - //give 1 card to the dealer and 2 to each player - Dealer.Cards.Add(Deck.Draw()); - foreach (var usr in Players) - { - usr.Cards.Add(Deck.Draw()); - usr.Cards.Add(Deck.Draw()); - - if (usr.GetHandValue() == 21) - usr.State = User.UserState.Blackjack; - } - - //go through all users and ask them what they want to do - foreach (var usr in Players.Where(x => !x.Done)) - { - while (!usr.Done) - { - Log.Information("Waiting for {DiscordUser}'s move", usr.DiscordUser); - await PromptUserMove(usr); - } - } - - await PrintState(); - State = GameState.Ended; - await Task.Delay(2500); - Log.Information("Dealer moves"); - await DealerMoves(); - await PrintState(); - _ = GameEnded?.Invoke(this); - } - catch (Exception ex) - { - Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE"); - State = GameState.Ended; - _ = GameEnded?.Invoke(this); - } - } - - private async Task PromptUserMove(User usr) - { - using var cts = new CancellationTokenSource(); - var pause = Task.Delay(20000, cts.Token); //10 seconds to decide - CurrentUser = usr; - currentUserMove = new(); - await PrintState(); - // either wait for the user to make an action and - // if he doesn't - stand - var finished = await Task.WhenAny(pause, currentUserMove.Task); - if (finished == pause) - await Stand(usr); - else - cts.Cancel(); - - CurrentUser = null; - currentUserMove = null; - } - - public async Task Join(IUser user, long bet) - { - await _locker.WaitAsync(); - try - { - if (State != GameState.Starting) - return false; - - if (Players.Count >= 5) - return false; - - if (!await _cs.RemoveAsync(user, bet, new("blackjack", "gamble"))) - return false; - - Players.Add(new(user, bet)); - _ = PrintState(); - return true; - } - finally - { - _locker.Release(); - } - } - - public async Task Stand(IUser u) - { - var cu = CurrentUser; - - if (cu is not null && cu.DiscordUser == u) - return await Stand(cu); - - return false; - } - - public async Task Stand(User u) - { - await _locker.WaitAsync(); - try - { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - u.State = User.UserState.Stand; - currentUserMove.TrySetResult(true); - return true; - } - finally - { - _locker.Release(); - } - } - - private async Task DealerMoves() - { - var hw = Dealer.GetHandValue(); - while (hw < 17 - || (hw == 17 - && Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10)) // hit on soft 17 - { - /* Dealer has - A 6 - That's 17, soft - hw == 17 => true - number of aces = 1 - 1 > 17-17 /10 => true - - AA 5 - That's 17, again soft, since one ace is worth 11, even though another one is 1 - hw == 17 => true - number of aces = 2 - 2 > 27 - 17 / 10 => true - - AA Q 5 - That's 17, but not soft, since both aces are worth 1 - hw == 17 => true - number of aces = 2 - 2 > 37 - 17 / 10 => false - * */ - Dealer.Cards.Add(Deck.Draw()); - hw = Dealer.GetHandValue(); - } - - if (hw > 21) - { - foreach (var usr in Players) - { - if (usr.State is User.UserState.Stand or User.UserState.Blackjack) - usr.State = User.UserState.Won; - else - usr.State = User.UserState.Lost; - } - } - else - { - foreach (var usr in Players) - { - if (usr.State == User.UserState.Blackjack) - usr.State = User.UserState.Won; - else if (usr.State == User.UserState.Stand) - usr.State = hw < usr.GetHandValue() ? User.UserState.Won : User.UserState.Lost; - else - usr.State = User.UserState.Lost; - } - } - - foreach (var usr in Players) - { - if (usr.State is User.UserState.Won or User.UserState.Blackjack) - await _cs.AddAsync(usr.DiscordUser.Id, usr.Bet * 2, new("blackjack", "win")); - } - } - - public async Task Double(IUser u) - { - var cu = CurrentUser; - - if (cu is not null && cu.DiscordUser == u) - return await Double(cu); - - return false; - } - - public async Task Double(User u) - { - await _locker.WaitAsync(); - try - { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - if (!await _cs.RemoveAsync(u.DiscordUser.Id, u.Bet, new("blackjack", "double"))) - return false; - - u.Bet *= 2; - - u.Cards.Add(Deck.Draw()); - - if (u.GetHandValue() == 21) - //blackjack - u.State = User.UserState.Blackjack; - else if (u.GetHandValue() > 21) - // user busted - u.State = User.UserState.Bust; - else - //with double you just get one card, and then you're done - u.State = User.UserState.Stand; - currentUserMove.TrySetResult(true); - - return true; - } - finally - { - _locker.Release(); - } - } - - public async Task Hit(IUser u) - { - var cu = CurrentUser; - - if (cu is not null && cu.DiscordUser == u) - return await Hit(cu); - - return false; - } - - public async Task Hit(User u) - { - await _locker.WaitAsync(); - try - { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - u.Cards.Add(Deck.Draw()); - - if (u.GetHandValue() == 21) - //blackjack - u.State = User.UserState.Blackjack; - else if (u.GetHandValue() > 21) - // user busted - u.State = User.UserState.Bust; - - currentUserMove.TrySetResult(true); - - return true; - } - finally - { - _locker.Release(); - } - } - - public Task PrintState() - { - if (StateUpdated is null) - return Task.CompletedTask; - return StateUpdated.Invoke(this); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Player.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Player.cs deleted file mode 100644 index fb6a2f8..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/BlackJack/Player.cs +++ /dev/null @@ -1,58 +0,0 @@ -#nullable disable -using Ellie.Econ; - -namespace Ellie.Modules.Gambling.Common.Blackjack; - -public abstract class Player -{ - public List Cards { get; } = new(); - - public int GetHandValue() - { - var val = GetRawHandValue(); - - // while the hand value is greater than 21, for each ace you have in the deck - // reduce the value by 10 until it drops below 22 - // (emulating the fact that ace is either a 1 or a 11) - var i = Cards.Count(x => x.Number == 1); - while (val > 21 && i-- > 0) - val -= 10; - return val; - } - - public int GetRawHandValue() - => Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number); -} - -public class Dealer : Player -{ -} - -public class User : Player -{ - public enum UserState - { - Waiting, - Stand, - Bust, - Blackjack, - Won, - Lost - } - - public UserState State { get; set; } = UserState.Waiting; - public long Bet { get; set; } - public IUser DiscordUser { get; } - - public bool Done - => State != UserState.Waiting; - - public User(IUser user, long bet) - { - if (bet <= 0) - throw new ArgumentOutOfRangeException(nameof(bet)); - - Bet = bet; - DiscordUser = user; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/CleanupCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/CleanupCommands.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4.cs deleted file mode 100644 index 6a608fa..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4.cs +++ /dev/null @@ -1,409 +0,0 @@ -#nullable disable -using CommandLine; -using System.Collections.Immutable; - -namespace Ellie.Modules.Gambling.Common.Connect4; - -public sealed class Connect4Game : IDisposable -{ - public enum Field //temporary most likely - { - Empty, - P1, - P2 - } - - public enum Phase - { - Joining, // waiting for second player to join - P1Move, - P2Move, - Ended - } - - public enum Result - { - Draw, - CurrentPlayerWon, - OtherPlayerWon - } - - public const int NUMBER_OF_COLUMNS = 7; - public const int NUMBER_OF_ROWS = 6; - - //public event Func OnGameStarted; - public event Func OnGameStateUpdated; - public event Func OnGameFailedToStart; - public event Func OnGameEnded; - - public Phase CurrentPhase { get; private set; } = Phase.Joining; - - public ImmutableArray GameState - => _gameState.ToImmutableArray(); - - public ImmutableArray<(ulong UserId, string Username)?> Players - => _players.ToImmutableArray(); - - public (ulong UserId, string Username) CurrentPlayer - => CurrentPhase == Phase.P1Move ? _players[0].Value : _players[1].Value; - - public (ulong UserId, string Username) OtherPlayer - => CurrentPhase == Phase.P2Move ? _players[0].Value : _players[1].Value; - - //state is bottom to top, left to right - private readonly Field[] _gameState = new Field[NUMBER_OF_ROWS * NUMBER_OF_COLUMNS]; - private readonly (ulong UserId, string Username)?[] _players = new (ulong, string)?[2]; - - private readonly SemaphoreSlim _locker = new(1, 1); - private readonly Options _options; - private readonly ICurrencyService _cs; - private readonly EllieRandom _rng; - - private Timer playerTimeoutTimer; - - /* [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - */ - - public Connect4Game( - ulong userId, - string userName, - Options options, - ICurrencyService cs) - { - _players[0] = (userId, userName); - _options = options; - _cs = cs; - - _rng = new(); - for (var i = 0; i < NUMBER_OF_COLUMNS * NUMBER_OF_ROWS; i++) - _gameState[i] = Field.Empty; - } - - public void Initialize() - { - if (CurrentPhase != Phase.Joining) - return; - _ = Task.Run(async () => - { - await Task.Delay(15000); - await _locker.WaitAsync(); - try - { - if (_players[1] is null) - { - _ = OnGameFailedToStart?.Invoke(this); - CurrentPhase = Phase.Ended; - await _cs.AddAsync(_players[0].Value.UserId, _options.Bet, new("connect4", "refund")); - } - } - finally { _locker.Release(); } - }); - } - - public async Task Join(ulong userId, string userName, int bet) - { - await _locker.WaitAsync(); - try - { - if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase - return false; - - if (_players[0].Value.UserId == userId) // same user can't join own game - return false; - - if (bet != _options.Bet) // can't join if bet amount is not the same - return false; - - if (!await _cs.RemoveAsync(userId, bet, new("connect4", "bet"))) // user doesn't have enough money to gamble - return false; - - if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player - { - _players[1] = _players[0]; - _players[0] = (userId, userName); - } - else //else join as a second player - _players[1] = (userId, userName); - - CurrentPhase = Phase.P1Move; //start the game - playerTimeoutTimer = new(async _ => - { - await _locker.WaitAsync(); - try - { - EndGame(Result.OtherPlayerWon, OtherPlayer.UserId); - } - finally { _locker.Release(); } - }, - null, - TimeSpan.FromSeconds(_options.TurnTimer), - TimeSpan.FromSeconds(_options.TurnTimer)); - _ = OnGameStateUpdated?.Invoke(this); - - return true; - } - finally { _locker.Release(); } - } - - public async Task Input(ulong userId, int inputCol) - { - await _locker.WaitAsync(); - try - { - inputCol -= 1; - if (CurrentPhase is Phase.Ended or Phase.Joining) - return false; - - if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move) - || (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move))) - return false; - - if (inputCol is < 0 or > NUMBER_OF_COLUMNS) //invalid input - return false; - - if (IsColumnFull(inputCol)) //can't play there event? - return false; - - var start = NUMBER_OF_ROWS * inputCol; - for (var i = start; i < start + NUMBER_OF_ROWS; i++) - { - if (_gameState[i] == Field.Empty) - { - _gameState[i] = GetPlayerPiece(userId); - break; - } - } - - //check winnning condition - // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected - - for (var i = 0; i < NUMBER_OF_ROWS - 3; i++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (var j = 0; j < NUMBER_OF_COLUMNS; j++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[i + (j * NUMBER_OF_ROWS)]; - if (first != Field.Empty) - { - for (var k = 1; k < 4; k++) - { - var next = _gameState[i + k + (j * NUMBER_OF_ROWS)]; - if (next == first) - { - if (k == 3) - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - else - continue; - } - else - break; - } - } - } - } - - // i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected - for (var i = 0; i < NUMBER_OF_COLUMNS - 3; i++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (var j = 0; j < NUMBER_OF_ROWS; j++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[j + (i * NUMBER_OF_ROWS)]; - if (first != Field.Empty) - { - for (var k = 1; k < 4; k++) - { - var next = _gameState[j + ((i + k) * NUMBER_OF_ROWS)]; - if (next == first) - { - if (k == 3) - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - else - continue; - } - else - break; - } - } - } - } - - //need to check diagonal now - for (var col = 0; col < NUMBER_OF_COLUMNS; col++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (var row = 0; row < NUMBER_OF_ROWS; row++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[row + (col * NUMBER_OF_ROWS)]; - - if (first != Field.Empty) - { - var same = 1; - - //top left - for (var i = 1; i < 4; i++) - { - //while going top left, rows are increasing, columns are decreasing - var curRow = row + i; - var curCol = col - i; - - //check if current values are in range - if (curRow is >= NUMBER_OF_ROWS or < 0) - break; - if (curCol is < 0 or >= NUMBER_OF_COLUMNS) - break; - - var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)]; - if (cur == first) - same++; - else - break; - } - - if (same == 4) - { - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - break; - } - - same = 1; - - //top right - for (var i = 1; i < 4; i++) - { - //while going top right, rows are increasing, columns are increasing - var curRow = row + i; - var curCol = col + i; - - //check if current values are in range - if (curRow is >= NUMBER_OF_ROWS or < 0) - break; - if (curCol is < 0 or >= NUMBER_OF_COLUMNS) - break; - - var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)]; - if (cur == first) - same++; - else - break; - } - - if (same == 4) - { - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - break; - } - } - } - } - - //check draw? if it's even possible - if (_gameState.All(x => x != Field.Empty)) - EndGame(Result.Draw, null); - - if (CurrentPhase != Phase.Ended) - { - if (CurrentPhase == Phase.P1Move) - CurrentPhase = Phase.P2Move; - else - CurrentPhase = Phase.P1Move; - - ResetTimer(); - } - - _ = OnGameStateUpdated?.Invoke(this); - return true; - } - finally { _locker.Release(); } - } - - private void ResetTimer() - => playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer), - TimeSpan.FromSeconds(_options.TurnTimer)); - - private void EndGame(Result result, ulong? winId) - { - if (CurrentPhase == Phase.Ended) - return; - _ = OnGameEnded?.Invoke(this, result); - CurrentPhase = Phase.Ended; - - if (result == Result.Draw) - { - _cs.AddAsync(CurrentPlayer.UserId, _options.Bet, new("connect4", "draw")); - _cs.AddAsync(OtherPlayer.UserId, _options.Bet, new("connect4", "draw")); - return; - } - - if (winId is not null) - _cs.AddAsync(winId.Value, (long)(_options.Bet * 1.98), new("connect4", "win")); - } - - private Field GetPlayerPiece(ulong userId) - => _players[0].Value.UserId == userId ? Field.P1 : Field.P2; - - //column is full if there are no empty fields - private bool IsColumnFull(int column) - { - var start = NUMBER_OF_ROWS * column; - for (var i = start; i < start + NUMBER_OF_ROWS; i++) - { - if (_gameState[i] == Field.Empty) - return false; - } - - return true; - } - - public void Dispose() - { - OnGameFailedToStart = null; - OnGameStateUpdated = null; - OnGameEnded = null; - playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite); - } - - - public class Options : IEllieCommandOptions - { - [Option('t', - "turn-timer", - Required = false, - Default = 15, - HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")] - public int TurnTimer { get; set; } = 15; - - [Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")] - public int Bet { get; set; } - - public void NormalizeOptions() - { - if (TurnTimer is < 5 or > 60) - TurnTimer = 15; - - if (Bet < 0) - Bet = 0; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4Commands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4Commands.cs deleted file mode 100644 index 57ecdf0..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Connect4/Connect4Commands.cs +++ /dev/null @@ -1,204 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Connect4; -using Ellie.Modules.Gambling.Services; -using System.Text; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class Connect4Commands : GamblingSubmodule - { - private static readonly string[] _numbers = - { - ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:" - }; - - private int RepostCounter - { - get => repostCounter; - set - { - if (value is < 0 or > 7) - repostCounter = 0; - else - repostCounter = value; - } - } - - private readonly DiscordSocketClient _client; - private readonly ICurrencyService _cs; - - private IUserMessage msg; - - private int repostCounter; - - public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb) - : base(gamb) - { - _client = client; - _cs = cs; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public async Task Connect4(params string[] args) - { - var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args); - if (!await CheckBetOptional(options.Bet)) - return; - - var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs); - Connect4Game game; - if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame) - { - if (game.CurrentPhase != Connect4Game.Phase.Joining) - return; - - newGame.Dispose(); - //means game already exists, try to join - await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet); - return; - } - - if (options.Bet > 0) - { - if (!await _cs.RemoveAsync(ctx.User.Id, options.Bet, new("connect4", "bet"))) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - _service.Connect4Games.TryRemove(ctx.Channel.Id, out _); - game.Dispose(); - return; - } - } - - game.OnGameStateUpdated += Game_OnGameStateUpdated; - game.OnGameFailedToStart += GameOnGameFailedToStart; - game.OnGameEnded += GameOnGameEnded; - _client.MessageReceived += ClientMessageReceived; - - game.Initialize(); - if (options.Bet == 0) - await ReplyConfirmLocalizedAsync(strs.connect4_created); - else - await ReplyErrorLocalizedAsync(strs.connect4_created_bet(N(options.Bet))); - - Task ClientMessageReceived(SocketMessage arg) - { - if (ctx.Channel.Id != arg.Channel.Id) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - var success = false; - if (int.TryParse(arg.Content, out var col)) - success = await game.Input(arg.Author.Id, col); - - if (success) - { - try { await arg.DeleteAsync(); } - catch { } - } - else - { - if (game.CurrentPhase is Connect4Game.Phase.Joining or Connect4Game.Phase.Ended) - return; - RepostCounter++; - if (RepostCounter == 0) - { - try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); } - catch { } - } - } - }); - return Task.CompletedTask; - } - - Task GameOnGameFailedToStart(Connect4Game arg) - { - if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) - { - _client.MessageReceived -= ClientMessageReceived; - toDispose.Dispose(); - } - - return ErrorLocalizedAsync(strs.connect4_failed_to_start); - } - - Task GameOnGameEnded(Connect4Game arg, Connect4Game.Result result) - { - if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) - { - _client.MessageReceived -= ClientMessageReceived; - toDispose.Dispose(); - } - - string title; - if (result == Connect4Game.Result.CurrentPlayerWon) - { - title = GetText(strs.connect4_won(Format.Bold(arg.CurrentPlayer.Username), - Format.Bold(arg.OtherPlayer.Username))); - } - else if (result == Connect4Game.Result.OtherPlayerWon) - { - title = GetText(strs.connect4_won(Format.Bold(arg.OtherPlayer.Username), - Format.Bold(arg.CurrentPlayer.Username))); - } - else - title = GetText(strs.connect4_draw); - - return msg.ModifyAsync(x => x.Embed = _eb.Create() - .WithTitle(title) - .WithDescription(GetGameStateText(game)) - .WithOkColor() - .Build()); - } - } - - private async Task Game_OnGameStateUpdated(Connect4Game game) - { - var embed = _eb.Create() - .WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}") - .WithDescription(GetGameStateText(game)) - .WithOkColor(); - - - if (msg is null) - msg = await ctx.Channel.EmbedAsync(embed); - else - await msg.ModifyAsync(x => x.Embed = embed.Build()); - } - - private string GetGameStateText(Connect4Game game) - { - var sb = new StringBuilder(); - - if (game.CurrentPhase is Connect4Game.Phase.P1Move or Connect4Game.Phase.P2Move) - sb.AppendLine(GetText(strs.connect4_player_to_move(Format.Bold(game.CurrentPlayer.Username)))); - - for (var i = Connect4Game.NUMBER_OF_ROWS; i > 0; i--) - { - for (var j = 0; j < Connect4Game.NUMBER_OF_COLUMNS; j++) - { - var cur = game.GameState[i + (j * Connect4Game.NUMBER_OF_ROWS) - 1]; - - if (cur == Connect4Game.Field.Empty) - sb.Append("⚫"); //black circle - else if (cur == Connect4Game.Field.P1) - sb.Append("🔴"); //red circle - else - sb.Append("🔵"); //blue circle - } - - sb.AppendLine(); - } - - for (var i = 0; i < Connect4Game.NUMBER_OF_COLUMNS; i++) - sb.Append(_numbers[i]); - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/DiceRoll/DiceRollCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/DiceRoll/DiceRollCommands.cs deleted file mode 100644 index fe7b91e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/DiceRoll/DiceRollCommands.cs +++ /dev/null @@ -1,224 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using System.Text.RegularExpressions; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class DiceRollCommands : EllieModule - { - private static readonly Regex _dndRegex = new(@"^(?\d+)d(?\d+)(?:\+(?\d+))?(?:\-(?\d+))?$", - RegexOptions.Compiled); - - private static readonly Regex _fudgeRegex = new(@"^(?\d+)d(?:F|f)$", RegexOptions.Compiled); - - private static readonly char[] _fateRolls = { '-', ' ', '+' }; - private readonly IImageCache _images; - - public DiceRollCommands(IImageCache images) - => _images = images; - - [Cmd] - public async Task Roll() - { - var rng = new EllieRandom(); - var gen = rng.Next(1, 101); - - var num1 = gen / 10; - var num2 = gen % 10; - - using var img1 = await GetDiceAsync(num1); - using var img2 = await GetDiceAsync(num2); - using var img = new[] { img1, img2 }.Merge(out var format); - await using var ms = await img.ToStreamAsync(format); - - var fileName = $"dice.{format.FileExtensions.First()}"; - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithAuthor(ctx.User) - .AddField(GetText(strs.roll2), gen) - .WithImageUrl($"attachment://{fileName}"); - - await ctx.Channel.SendFileAsync(ms, - fileName, - embed: eb.Build()); - } - - [Cmd] - [Priority(1)] - public async Task Roll(int num) - => await InternalRoll(num, true); - - - [Cmd] - [Priority(1)] - public async Task Rolluo(int num = 1) - => await InternalRoll(num, false); - - [Cmd] - [Priority(0)] - public async Task Roll(string arg) - => await InternallDndRoll(arg, true); - - [Cmd] - [Priority(0)] - public async Task Rolluo(string arg) - => await InternallDndRoll(arg, false); - - private async Task InternalRoll(int num, bool ordered) - { - if (num is < 1 or > 30) - { - await ReplyErrorLocalizedAsync(strs.dice_invalid_number(1, 30)); - return; - } - - var rng = new EllieRandom(); - - var dice = new List>(num); - var values = new List(num); - for (var i = 0; i < num; i++) - { - var randomNumber = rng.Next(1, 7); - var toInsert = dice.Count; - if (ordered) - { - if (randomNumber == 6 || dice.Count == 0) - toInsert = 0; - else if (randomNumber != 1) - { - for (var j = 0; j < dice.Count; j++) - { - if (values[j] < randomNumber) - { - toInsert = j; - break; - } - } - } - } - else - toInsert = dice.Count; - - dice.Insert(toInsert, await GetDiceAsync(randomNumber)); - values.Insert(toInsert, randomNumber); - } - - using var bitmap = dice.Merge(out var format); - await using var ms = bitmap.ToStream(format); - foreach (var d in dice) - d.Dispose(); - - var imageName = $"dice.{format.FileExtensions.First()}"; - var eb = _eb.Create(ctx) - .WithOkColor() - .WithAuthor(ctx.User) - .AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true) - .AddField(GetText(strs.total), values.Sum(), true) - .WithDescription(GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString())))) - .WithImageUrl($"attachment://{imageName}"); - - await ctx.Channel.SendFileAsync(ms, - imageName, - embed: eb.Build()); - } - - private async Task InternallDndRoll(string arg, bool ordered) - { - Match match; - if ((match = _fudgeRegex.Match(arg)).Length != 0 - && int.TryParse(match.Groups["n1"].ToString(), out var n1) - && n1 is > 0 and < 500) - { - var rng = new EllieRandom(); - - var rolls = new List(); - - for (var i = 0; i < n1; i++) - rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]); - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(ctx.User) - .WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString())))) - .AddField(Format.Bold("Result"), - string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))); - - await ctx.Channel.EmbedAsync(embed); - } - else if ((match = _dndRegex.Match(arg)).Length != 0) - { - var rng = new EllieRandom(); - if (int.TryParse(match.Groups["n1"].ToString(), out n1) - && int.TryParse(match.Groups["n2"].ToString(), out var n2) - && n1 <= 50 - && n2 <= 100000 - && n1 > 0 - && n2 > 0) - { - if (!int.TryParse(match.Groups["add"].Value, out var add)) - add = 0; - if (!int.TryParse(match.Groups["sub"].Value, out var sub)) - sub = 0; - - var arr = new int[n1]; - for (var i = 0; i < n1; i++) - arr[i] = rng.Next(1, n2 + 1); - - var sum = arr.Sum(); - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(ctx.User) - .WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`"))) - .AddField(Format.Bold(GetText(strs.rolls)), - string.Join(" ", - (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x - => Format.Code(x.ToString())))) - .AddField(Format.Bold("Sum"), - sum + " + " + add + " - " + sub + " = " + (sum + add - sub)); - await ctx.Channel.EmbedAsync(embed); - } - } - } - - [Cmd] - public async Task NRoll([Leftover] string range) - { - int rolled; - if (range.Contains("-")) - { - var arr = range.Split('-').Take(2).Select(int.Parse).ToArray(); - if (arr[0] > arr[1]) - { - await ReplyErrorLocalizedAsync(strs.second_larger_than_first); - return; - } - - rolled = new EllieRandom().Next(arr[0], arr[1] + 1); - } - else - rolled = new EllieRandom().Next(0, int.Parse(range) + 1); - - await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString()))); - } - - private async Task> GetDiceAsync(int num) - { - if (num is < 0 or > 10) - throw new ArgumentOutOfRangeException(nameof(num)); - - if (num == 10) - { - using var imgOne = Image.Load(await _images.GetDiceAsync(1)); - using var imgZero = Image.Load(await _images.GetDiceAsync(0)); - return new[] { imgOne, imgZero }.Merge(); - } - - return Image.Load(await _images.GetDiceAsync(num)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Draw/DrawCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Draw/DrawCommands.cs deleted file mode 100644 index 2b9cf8c..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Draw/DrawCommands.cs +++ /dev/null @@ -1,234 +0,0 @@ -#nullable disable -using Ellie.Econ; -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class DrawCommands : GamblingSubmodule - { - private static readonly ConcurrentDictionary _allDecks = new(); - private readonly IImageCache _images; - - public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs) - => _images = images; - - private async Task InternalDraw(int count, ulong? guildId = null) - { - if (count is < 1 or > 10) - throw new ArgumentOutOfRangeException(nameof(count)); - - var cards = guildId is null ? new() : _allDecks.GetOrAdd(ctx.Guild, _ => new()); - var images = new List>(); - var cardObjects = new List(); - for (var i = 0; i < count; i++) - { - if (cards.CardPool.Count == 0 && i != 0) - { - try - { - await ReplyErrorLocalizedAsync(strs.no_more_cards); - } - catch - { - // ignored - } - - break; - } - - var currentCard = cards.Draw(); - cardObjects.Add(currentCard); - var image = await GetCardImageAsync(currentCard); - images.Add(image); - } - - var imgName = "cards.jpg"; - using var img = images.Merge(); - foreach (var i in images) - i.Dispose(); - - var eb = _eb.Create(ctx) - .WithOkColor(); - - var toSend = string.Empty; - if (cardObjects.Count == 5) - eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true); - - if (guildId is not null) - toSend += GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString()))); - - eb.WithDescription(toSend) - .WithAuthor(ctx.User) - .WithImageUrl($"attachment://{imgName}"); - - if (count > 1) - eb.AddField(GetText(strs.cards), count.ToString(), true); - - await using var imageStream = await img.ToStreamAsync(); - await ctx.Channel.SendFileAsync(imageStream, - imgName, - embed: eb.Build()); - } - - private async Task> GetCardImageAsync(RegularCard currentCard) - { - var cardName = currentCard.GetName().ToLowerInvariant().Replace(' ', '_'); - var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg"); - return Image.Load(cardBytes); - } - - private async Task> GetCardImageAsync(Deck.Card currentCard) - { - var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_'); - var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg"); - return Image.Load(cardBytes); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Draw(int num = 1) - { - if (num < 1) - return; - - if (num > 10) - num = 10; - - await InternalDraw(num, ctx.Guild.Id); - } - - [Cmd] - public async Task DrawNew(int num = 1) - { - if (num < 1) - return; - - if (num > 10) - num = 10; - - await InternalDraw(num); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task DeckShuffle() - { - //var channel = (ITextChannel)ctx.Channel; - - _allDecks.AddOrUpdate(ctx.Guild, - _ => new(), - (_, c) => - { - c.Restart(); - return c; - }); - - await ReplyConfirmLocalizedAsync(strs.deck_reshuffled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputValueGuess val, InputColorGuess? col = null) - => BetDrawInternal(amount, val, col); - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputColorGuess col, InputValueGuess? val = null) - => BetDrawInternal(amount, val, col); - - public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col) - { - if (amount <= 0) - return; - - var res = await _service.BetDrawAsync(ctx.User.Id, - amount, - (byte?)val, - (byte?)col); - - if (!res.TryPickT0(out var result, out _)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithAuthor(ctx.User) - .WithDescription(result.Card.GetEmoji()) - .AddField(GetText(strs.guess), GetGuessInfo(val, col), true) - .AddField(GetText(strs.card), GetCardInfo(result.Card), true) - .AddField(GetText(strs.won), N((long)result.Won), false) - .WithImageUrl("attachment://card.png"); - - using var img = await GetCardImageAsync(result.Card); - await using var imgStream = await img.ToStreamAsync(); - await ctx.Channel.SendFileAsync(imgStream, "card.png", embed: eb.Build()); - } - - private string GetGuessInfo(InputValueGuess? valG, InputColorGuess? colG) - { - var val = valG switch - { - InputValueGuess.H => "Hi ⬆️", - InputValueGuess.L => "Lo ⬇️", - _ => "❓" - }; - - var col = colG switch - { - InputColorGuess.Red => "R 🔴", - InputColorGuess.Black => "B ⚫", - _ => "❓" - }; - - return $"{val} / {col}"; - } - private string GetCardInfo(RegularCard card) - { - var val = (int)card.Value switch - { - < 7 => "Lo ⬇️", - > 7 => "Hi ⬆️", - _ => "7 💀" - }; - - var col = card.Value == RegularValue.Seven - ? "7 💀" - : card.Suit switch - { - RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴", - _ => "B ⚫" - }; - - return $"{val} / {col}"; - } - - public enum InputValueGuess - { - High = 0, - H = 0, - Hi = 0, - Low = 1, - L = 1, - Lo = 1, - } - - public enum InputColorGuess - { - R = 0, - Red = 0, - B = 1, - Bl = 1, - Black = 1, - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/EconomyResult.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/EconomyResult.cs deleted file mode 100644 index 811d1c5..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/EconomyResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Services; - -public sealed class EconomyResult -{ - public decimal Cash { get; init; } - public decimal Planted { get; init; } - public decimal Waifus { get; init; } - public decimal OnePercent { get; init; } - public decimal Bank { get; init; } - public long Bot { get; init; } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsCommands.cs deleted file mode 100644 index fd0f655..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsCommands.cs +++ /dev/null @@ -1,60 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Events; -using Ellie.Modules.Gambling.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class CurrencyEventsCommands : GamblingSubmodule - { - public CurrencyEventsCommands(GamblingConfigService gamblingConf) - : base(gamblingConf) - { - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - [OwnerOnly] - public async Task EventStart(CurrencyEvent.Type ev, params string[] options) - { - var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options); - if (!await _service.TryCreateEventAsync(ctx.Guild.Id, ctx.Channel.Id, ev, opts, GetEmbed)) - await ReplyErrorLocalizedAsync(strs.start_event_fail); - } - - private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot) - => type switch - { - CurrencyEvent.Type.Reaction => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.event_title(type.ToString()))) - .WithDescription(GetReactionDescription(opts.Amount, currentPot)) - .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), - CurrencyEvent.Type.GameStatus => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.event_title(type.ToString()))) - .WithDescription(GetGameStatusDescription(opts.Amount, currentPot)) - .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }; - - private string GetReactionDescription(long amount, long potSize) - { - var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : N(potSize)); - - return GetText(strs.new_reaction_event(CurrencySign, Format.Bold(N(amount)), potSizeStr)); - } - - private string GetGameStatusDescription(long amount, long potSize) - { - var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : potSize + CurrencySign); - - return GetText(strs.new_gamestatus_event(CurrencySign, Format.Bold(N(amount)), potSizeStr)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsService.cs deleted file mode 100644 index be62459..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/CurrencyEventsService.cs +++ /dev/null @@ -1,67 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Events; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling.Services; - -public class CurrencyEventsService : IEService -{ - private readonly DiscordSocketClient _client; - private readonly ICurrencyService _cs; - private readonly GamblingConfigService _configService; - - private readonly ConcurrentDictionary _events = new(); - - public CurrencyEventsService(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService configService) - { - _client = client; - _cs = cs; - _configService = configService; - } - - public async Task TryCreateEventAsync( - ulong guildId, - ulong channelId, - CurrencyEvent.Type type, - EventOptions opts, - Func embed) - { - var g = _client.GetGuild(guildId); - if (g?.GetChannel(channelId) is not ITextChannel ch) - return false; - - ICurrencyEvent ce; - - if (type == CurrencyEvent.Type.Reaction) - ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed); - else if (type == CurrencyEvent.Type.GameStatus) - ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed); - else - return false; - - var added = _events.TryAdd(guildId, ce); - if (added) - { - try - { - ce.OnEnded += OnEventEnded; - await ce.StartEvent(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error starting event"); - _events.TryRemove(guildId, out ce); - return false; - } - } - - return added; - } - - private Task OnEventEnded(ulong gid) - { - _events.TryRemove(gid, out _); - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/EventOptions.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/EventOptions.cs deleted file mode 100644 index 163f9a9..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/EventOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable disable -using CommandLine; - -namespace Ellie.Modules.Gambling.Common.Events; - -public class EventOptions : IEllieCommandOptions -{ - [Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")] - public long Amount { get; set; } = 100; - - [Option('p', - "pot-size", - Required = false, - Default = 0, - HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")] - public long PotSize { get; set; } - - //[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")] - //public string TypeString { get; set; } = "reaction"; - [Option('d', - "duration", - Required = false, - Default = 24, - HelpText = "Number of hours the event should run for. Default 24.")] - public int Hours { get; set; } = 24; - - - public void NormalizeOptions() - { - if (Amount < 0) - Amount = 100; - if (PotSize < 0) - PotSize = 0; - if (Hours <= 0) - Hours = 24; - if (PotSize != 0 && PotSize < Amount) - PotSize = 0; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/GameStatusEvent.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/GameStatusEvent.cs deleted file mode 100644 index 10ec416..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/GameStatusEvent.cs +++ /dev/null @@ -1,195 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; -using System.Collections.Concurrent; - -namespace Ellie.Modules.Gambling.Common.Events; - -public class GameStatusEvent : ICurrencyEvent -{ - public event Func OnEnded; - private long PotSize { get; set; } - public bool Stopped { get; private set; } - public bool PotEmptied { get; private set; } - private readonly DiscordSocketClient _client; - private readonly IGuild _guild; - private IUserMessage msg; - private readonly ICurrencyService _cs; - private readonly long _amount; - - private readonly Func _embedFunc; - private readonly bool _isPotLimited; - private readonly ITextChannel _channel; - private readonly ConcurrentHashSet _awardedUsers = new(); - private readonly ConcurrentQueue _toAward = new(); - private readonly Timer _t; - private readonly Timer _timeout; - private readonly EventOptions _opts; - - private readonly string _code; - - private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10) - .Concat(Enumerable.Range(65, 26)) - .Concat(Enumerable.Range(97, 26)) - .Select(x => (char)x) - .ToArray(); - - private readonly object _stopLock = new(); - - private readonly object _potLock = new(); - - public GameStatusEvent( - DiscordSocketClient client, - ICurrencyService cs, - SocketGuild g, - ITextChannel ch, - EventOptions opt, - Func embedFunc) - { - _client = client; - _guild = g; - _cs = cs; - _amount = opt.Amount; - PotSize = opt.PotSize; - _embedFunc = embedFunc; - _isPotLimited = PotSize > 0; - _channel = ch; - _opts = opt; - // generate code - _code = new(_sneakyGameStatusChars.Shuffle().Take(5).ToArray()); - - _t = new(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); - if (_opts.Hours > 0) - _timeout = new(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); - } - - private void EventTimeout(object state) - => _ = StopEvent(); - - private async void OnTimerTick(object state) - { - var potEmpty = PotEmptied; - var toAward = new List(); - while (_toAward.TryDequeue(out var x)) - toAward.Add(x); - - if (!toAward.Any()) - return; - - try - { - await _cs.AddBulkAsync(toAward, - _amount, - new("event", "gamestatus") - ); - - if (_isPotLimited) - { - await msg.ModifyAsync(m => - { - m.Embed = GetEmbed(PotSize).Build(); - }); - } - - Log.Information("Game status event awarded {Count} users {Amount} currency.{Remaining}", - toAward.Count, - _amount, - _isPotLimited ? $" {PotSize} left." : ""); - - if (potEmpty) - _ = StopEvent(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in OnTimerTick in gamestatusevent"); - } - } - - public async Task StartEvent() - { - msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)); - await _client.SetGameAsync(_code); - _client.MessageDeleted += OnMessageDeleted; - _client.MessageReceived += HandleMessage; - _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); - } - - private IEmbedBuilder GetEmbed(long pot) - => _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot); - - private async Task OnMessageDeleted(Cacheable message, Cacheable cacheable) - { - if (message.Id == msg.Id) - await StopEvent(); - } - - public Task StopEvent() - { - lock (_stopLock) - { - if (Stopped) - return Task.CompletedTask; - Stopped = true; - _client.MessageDeleted -= OnMessageDeleted; - _client.MessageReceived -= HandleMessage; - _t.Change(Timeout.Infinite, Timeout.Infinite); - _timeout?.Change(Timeout.Infinite, Timeout.Infinite); - _ = _client.SetGameAsync(null); - try - { - _ = msg.DeleteAsync(); - } - catch { } - - _ = OnEnded?.Invoke(_guild.Id); - } - - return Task.CompletedTask; - } - - private Task HandleMessage(SocketMessage message) - { - _ = Task.Run(async () => - { - if (message.Author is not IGuildUser gu // no unknown users, as they could be bots, or alts - || gu.IsBot // no bots - || message.Content != _code // code has to be the same - || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts - return; - // there has to be money left in the pot - // and the user wasn't rewarded - if (_awardedUsers.Add(message.Author.Id) && TryTakeFromPot()) - { - _toAward.Enqueue(message.Author.Id); - if (_isPotLimited && PotSize < _amount) - PotEmptied = true; - } - - try - { - await message.DeleteAsync(new() - { - RetryMode = RetryMode.AlwaysFail - }); - } - catch { } - }); - return Task.CompletedTask; - } - - private bool TryTakeFromPot() - { - if (_isPotLimited) - { - lock (_potLock) - { - if (PotSize < _amount) - return false; - - PotSize -= _amount; - return true; - } - } - - return true; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ICurrencyEvent.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ICurrencyEvent.cs deleted file mode 100644 index fdb0431..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ICurrencyEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common; - -public interface ICurrencyEvent -{ - event Func OnEnded; - Task StopEvent(); - Task StartEvent(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ReactionEvent.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ReactionEvent.cs deleted file mode 100644 index 85ecfe0..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Events/ReactionEvent.cs +++ /dev/null @@ -1,194 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling.Common.Events; - -public class ReactionEvent : ICurrencyEvent -{ - public event Func OnEnded; - private long PotSize { get; set; } - public bool Stopped { get; private set; } - public bool PotEmptied { get; private set; } - private readonly DiscordSocketClient _client; - private readonly IGuild _guild; - private IUserMessage msg; - private IEmote emote; - private readonly ICurrencyService _cs; - private readonly long _amount; - - private readonly Func _embedFunc; - private readonly bool _isPotLimited; - private readonly ITextChannel _channel; - private readonly ConcurrentHashSet _awardedUsers = new(); - private readonly System.Collections.Concurrent.ConcurrentQueue _toAward = new(); - private readonly Timer _t; - private readonly Timer _timeout; - private readonly bool _noRecentlyJoinedServer; - private readonly EventOptions _opts; - private readonly GamblingConfig _config; - - private readonly object _stopLock = new(); - - private readonly object _potLock = new(); - - public ReactionEvent( - DiscordSocketClient client, - ICurrencyService cs, - SocketGuild g, - ITextChannel ch, - EventOptions opt, - GamblingConfig config, - Func embedFunc) - { - _client = client; - _guild = g; - _cs = cs; - _amount = opt.Amount; - PotSize = opt.PotSize; - _embedFunc = embedFunc; - _isPotLimited = PotSize > 0; - _channel = ch; - _noRecentlyJoinedServer = false; - _opts = opt; - _config = config; - - _t = new(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); - if (_opts.Hours > 0) - _timeout = new(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); - } - - private void EventTimeout(object state) - => _ = StopEvent(); - - private async void OnTimerTick(object state) - { - var potEmpty = PotEmptied; - var toAward = new List(); - while (_toAward.TryDequeue(out var x)) - toAward.Add(x); - - if (!toAward.Any()) - return; - - try - { - await _cs.AddBulkAsync(toAward, _amount, new("event", "reaction")); - - if (_isPotLimited) - { - await msg.ModifyAsync(m => - { - m.Embed = GetEmbed(PotSize).Build(); - }); - } - - Log.Information("Reaction Event awarded {Count} users {Amount} currency.{Remaining}", - toAward.Count, - _amount, - _isPotLimited ? $" {PotSize} left." : ""); - - if (potEmpty) - _ = StopEvent(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error adding bulk currency to users"); - } - } - - public async Task StartEvent() - { - if (Emote.TryParse(_config.Currency.Sign, out var parsedEmote)) - emote = parsedEmote; - else - emote = new Emoji(_config.Currency.Sign); - msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)); - await msg.AddReactionAsync(emote); - _client.MessageDeleted += OnMessageDeleted; - _client.ReactionAdded += HandleReaction; - _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); - } - - private IEmbedBuilder GetEmbed(long pot) - => _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot); - - private async Task OnMessageDeleted(Cacheable message, Cacheable cacheable) - { - if (message.Id == msg.Id) - await StopEvent(); - } - - public Task StopEvent() - { - lock (_stopLock) - { - if (Stopped) - return Task.CompletedTask; - - Stopped = true; - _client.MessageDeleted -= OnMessageDeleted; - _client.ReactionAdded -= HandleReaction; - _t.Change(Timeout.Infinite, Timeout.Infinite); - _timeout?.Change(Timeout.Infinite, Timeout.Infinite); - try - { - _ = msg.DeleteAsync(); - } - catch { } - - _ = OnEnded?.Invoke(_guild.Id); - } - - return Task.CompletedTask; - } - - private Task HandleReaction( - Cacheable message, - Cacheable cacheable, - SocketReaction r) - { - _ = Task.Run(() => - { - if (emote.Name != r.Emote.Name) - return; - if ((r.User.IsSpecified - ? r.User.Value - : null) is not IGuildUser gu // no unknown users, as they could be bots, or alts - || message.Id != msg.Id // same message - || gu.IsBot // no bots - || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts - || (_noRecentlyJoinedServer - && // if specified, no users who joined the server in the last 24h - (gu.JoinedAt is null - || (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays - < 1))) // and no users for who we don't know when they joined - return; - // there has to be money left in the pot - // and the user wasn't rewarded - if (_awardedUsers.Add(r.UserId) && TryTakeFromPot()) - { - _toAward.Enqueue(r.UserId); - if (_isPotLimited && PotSize < _amount) - PotEmptied = true; - } - }); - return Task.CompletedTask; - } - - private bool TryTakeFromPot() - { - if (_isPotLimited) - { - lock (_potLock) - { - if (PotSize < _amount) - return false; - - PotSize -= _amount; - return true; - } - } - - return true; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipCoinCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipCoinCommands.cs deleted file mode 100644 index 0b759b5..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipCoinCommands.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class FlipCoinCommands : GamblingSubmodule - { - public enum BetFlipGuess : byte - { - H = 0, - Head = 0, - Heads = 0, - T = 1, - Tail = 1, - Tails = 1 - } - - private static readonly EllieRandom _rng = new(); - private readonly IImageCache _images; - private readonly ICurrencyService _cs; - private readonly ImagesConfig _ic; - - public FlipCoinCommands( - IImageCache images, - ImagesConfig ic, - ICurrencyService cs, - GamblingConfigService gss) - : base(gss) - { - _ic = ic; - _images = images; - _cs = cs; - } - - [Cmd] - public async Task Flip(int count = 1) - { - if (count is > 10 or < 1) - { - await ReplyErrorLocalizedAsync(strs.flip_invalid(10)); - return; - } - - var headCount = 0; - var tailCount = 0; - var imgs = new Image[count]; - var headsArr = await _images.GetHeadsImageAsync(); - var tailsArr = await _images.GetTailsImageAsync(); - - var result = await _service.FlipAsync(count); - - for (var i = 0; i < result.Length; i++) - { - if (result[i].Side == 0) - { - imgs[i] = Image.Load(headsArr); - headCount++; - } - else - { - imgs[i] = Image.Load(tailsArr); - tailCount++; - } - } - - using var img = imgs.Merge(out var format); - await using var stream = await img.ToStreamAsync(format); - foreach (var i in imgs) - i.Dispose(); - - var imgName = $"coins.{format.FileExtensions.First()}"; - - var msg = count != 1 - ? Format.Bold(GetText(strs.flip_results(count, headCount, tailCount))) - : GetText(strs.flipped(headCount > 0 - ? Format.Bold(GetText(strs.heads)) - : Format.Bold(GetText(strs.tails)))); - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithAuthor(ctx.User) - .WithDescription(msg) - .WithImageUrl($"attachment://{imgName}"); - - await ctx.Channel.SendFileAsync(stream, - imgName, - embed: eb.Build()); - } - - [Cmd] - public async Task Betflip([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, BetFlipGuess guess) - { - if (!await CheckBetMandatory(amount) || amount == 1) - return; - - var res = await _service.BetFlipAsync(ctx.User.Id, amount, (byte)guess); - if (!res.TryPickT0(out var result, out _)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - Uri imageToSend; - var coins = _ic.Data.Coins; - if (result.Side == 0) - { - imageToSend = coins.Heads[_rng.Next(0, coins.Heads.Length)]; - } - else - { - imageToSend = coins.Tails[_rng.Next(0, coins.Tails.Length)]; - } - - string str; - var won = (long)result.Won; - if (won > 0) - { - str = Format.Bold(GetText(strs.flip_guess(N(won)))); - } - else - { - str = Format.Bold(GetText(strs.better_luck)); - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithAuthor(ctx.User) - .WithDescription(str) - .WithOkColor() - .WithImageUrl(imageToSend.ToString())); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipResult.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipResult.cs deleted file mode 100644 index 044f991..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/FlipCoin/FlipResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Econ.Gambling; - -public readonly struct FlipResult -{ - public long Won { get; init; } - public int Side { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Gambling.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Gambling.cs deleted file mode 100644 index 204003b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Gambling.cs +++ /dev/null @@ -1,1014 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Gambling.Bank; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; -using Ellie.Modules.Utility.Services; -using Ellie.Services.Currency; -using Ellie.Services.Database.Models; -using System.Collections.Immutable; -using System.Globalization; -using System.Text; -using Ellie.Econ.Gambling.Rps; -using Ellie.Common.TypeReaders; -using Ellie.Modules.Patronage; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling : GamblingModule -{ - private readonly IGamblingService _gs; - private readonly DbService _db; - private readonly ICurrencyService _cs; - private readonly DiscordSocketClient _client; - private readonly NumberFormatInfo _enUsCulture; - private readonly DownloadTracker _tracker; - private readonly GamblingConfigService _configService; - private readonly IBankService _bank; - private readonly IPatronageService _ps; - private readonly IRemindService _remind; - private readonly GamblingTxTracker _gamblingTxTracker; - - private IUserMessage rdMsg; - - public Gambling( - IGamblingService gs, - DbService db, - ICurrencyService currency, - DiscordSocketClient client, - DownloadTracker tracker, - GamblingConfigService configService, - IBankService bank, - IPatronageService ps, - IRemindService remind, - GamblingTxTracker gamblingTxTracker) - : base(configService) - { - _gs = gs; - _db = db; - _cs = currency; - _client = client; - _bank = bank; - _ps = ps; - _remind = remind; - _gamblingTxTracker = gamblingTxTracker; - - _enUsCulture = new CultureInfo("en-US", false).NumberFormat; - _enUsCulture.NumberDecimalDigits = 0; - _enUsCulture.NumberGroupSeparator = " "; - _tracker = tracker; - _configService = configService; - } - - public async Task GetBalanceStringAsync(ulong userId) - { - var bal = await _cs.GetBalanceAsync(userId); - return N(bal); - } - - [Cmd] - public async Task BetStats() - { - var stats = await _gamblingTxTracker.GetAllAsync(); - - var eb = _eb.Create(ctx) - .WithOkColor(); - - var str = "` Feature `|`   Bet  `|`Paid Out`|`  RoI  `\n"; - str += "――――――――――――――――――――\n"; - foreach (var stat in stats) - { - var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture); - str += $"`{stat.Feature.PadBoth(9)}`" + - $"|`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`" + - $"|`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`" + - $"|`{perc.PadLeft(6, ' ')}`\n"; - } - - var bet = stats.Sum(x => x.Bet); - var paidOut = stats.Sum(x => x.PaidOut); - - if (bet == 0) - bet = 1; - - var tPerc = (paidOut / bet).ToString("P2", Culture); - str += "――――――――――――――――――――\n"; - str += $"` {("TOTAL").PadBoth(7)}` " + - $"|**{N(bet).PadLeft(8, ' ')}**" + - $"|**{N(paidOut).PadLeft(8, ' ')}**" + - $"|`{tPerc.PadLeft(6, ' ')}`"; - - eb.WithDescription(str); - - await ctx.Channel.EmbedAsync(eb); - } - - [Cmd] - public async Task Economy() - { - var ec = await _service.GetEconomyAsync(); - decimal onePercent = 0; - - // This stops the top 1% from owning more than 100% of the money - if (ec.Cash > 0) - { - onePercent = ec.OnePercent / (ec.Cash - ec.Bot); - } - - // [21:03] Bob Page: Kinda remids me of US economy - var embed = _eb.Create() - .WithTitle(GetText(strs.economy_state)) - .AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot)) - .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") - .AddField(GetText(strs.currency_planted), N(ec.Planted)) - .AddField(GetText(strs.owned_waifus_total), N(ec.Waifus)) - .AddField(GetText(strs.bot_currency), N(ec.Bot)) - .AddField(GetText(strs.bank_accounts), N(ec.Bank)) - .AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank)) - .WithOkColor(); - - // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table - await ctx.Channel.EmbedAsync(embed); - } - - private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey() - { - Key = "timely:extra_percent", - PrettyName = "Timely" - }; - - private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when) - { - var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative); - - await _remind.AddReminderAsync(ctx.User.Id, - ctx.User.Id, - ctx.Guild?.Id, - true, - when, - GetText(strs.timely_time), - ReminderType.Timely); - - await smc.RespondConfirmAsync(_eb, GetText(strs.remind_timely(tt)), ephemeral: true); - } - - private EllieInteraction CreateRemindMeInteraction(int period) - { - return _inter - .Create(ctx.User.Id, - new SimpleInteraction( - new ButtonBuilder( - label: "Remind me", - emote: Emoji.Parse("⏰"), - customId: "timely:remind_me"), - RemindTimelyAction, - DateTime.UtcNow.Add(TimeSpan.FromHours(period)))); - } - - [Cmd] - public async Task Timely() - { - var val = Config.Timely.Amount; - var period = Config.Timely.Cooldown; - if (val <= 0 || period <= 0) - { - await ReplyErrorLocalizedAsync(strs.timely_none); - return; - } - - var inter = CreateRemindMeInteraction(period); - - if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem) - { - // Removes timely button if there is a timely reminder in DB - if (_service.UserHasTimelyReminder(ctx.User.Id)) - { - inter = null; - } - - var now = DateTime.UtcNow; - var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative); - await ReplyPendingLocalizedAsync(strs.timely_already_claimed(relativeTag), inter); - 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), inter); - } - - [Cmd] - [OwnerOnly] - public async Task TimelyReset() - { - await _service.RemoveAllTimelyClaimsAsync(); - await ReplyConfirmLocalizedAsync(strs.timely_reset); - } - - [Cmd] - [OwnerOnly] - public async Task TimelySet(int amount, int period = 24) - { - if (amount < 0 || period < 0) - { - return; - } - - _configService.ModifyConfig(gs => - { - gs.Timely.Amount = amount; - gs.Timely.Cooldown = period; - }); - - if (amount == 0) - { - await ReplyConfirmLocalizedAsync(strs.timely_set_none); - } - else - { - await ReplyConfirmLocalizedAsync(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Raffle([Leftover] IRole role = null) - { - role ??= ctx.Guild.EveryoneRole; - - var members = (await role.GetMembersAsync()).Where(u => u.Status != UserStatus.Offline); - var membersArray = members as IUser[] ?? members.ToArray(); - if (membersArray.Length == 0) - { - return; - } - - var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)]; - await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), - $"**{usr.Username}#{usr.Discriminator}**", - footer: $"ID: {usr.Id}"); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RaffleAny([Leftover] IRole role = null) - { - role ??= ctx.Guild.EveryoneRole; - - var members = await role.GetMembersAsync(); - var membersArray = members as IUser[] ?? members.ToArray(); - if (membersArray.Length == 0) - { - return; - } - - var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)]; - await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), - $"**{usr.Username}#{usr.Discriminator}**", - footer: $"ID: {usr.Id}"); - } - - [Cmd] - [Priority(2)] - public Task CurrencyTransactions(int page = 1) - => InternalCurrencyTransactions(ctx.User.Id, page); - - [Cmd] - [OwnerOnly] - [Priority(0)] - public Task CurrencyTransactions([Leftover] IUser usr) - => InternalCurrencyTransactions(usr.Id, 1); - - [Cmd] - [OwnerOnly] - [Priority(1)] - public Task CurrencyTransactions(IUser usr, int page) - => InternalCurrencyTransactions(usr.Id, page); - - private async Task InternalCurrencyTransactions(ulong userId, int page) - { - if (--page < 0) - { - return; - } - - List trs; - await using (var uow = _db.GetDbContext()) - { - trs = await uow.Set().GetPageFor(userId, page); - } - - var embed = _eb.Create() - .WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() - ?? $"{userId}"))) - .WithOkColor(); - - var sb = new StringBuilder(); - foreach (var tr in trs) - { - var change = tr.Amount >= 0 ? "🔵" : "🔴"; - var kwumId = new kwum(tr.Id).ToString(); - var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`"; - - sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}"); - var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId); - if (transactionString is not null) - { - sb.AppendLine(transactionString); - } - - if (!string.IsNullOrWhiteSpace(tr.Note)) - { - sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}"); - } - } - - embed.WithDescription(sb.ToString()); - embed.WithFooter(GetText(strs.page(page + 1))); - await ctx.Channel.EmbedAsync(embed); - } - - private static string GetFormattedCurtrDate(CurrencyTransaction ct) - => $"{ct.DateAdded:HH:mm yyyy-MM-dd}"; - - [Cmd] - public async Task CurrencyTransaction(kwum id) - { - int intId = id; - await using var uow = _db.GetDbContext(); - - var tr = await uow.Set().ToLinqToDBTable() - .Where(x => x.Id == intId && x.UserId == ctx.User.Id) - .FirstOrDefaultAsync(); - - if (tr is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - var eb = _eb.Create(ctx).WithOkColor(); - - eb.WithAuthor(ctx.User); - eb.WithTitle(GetText(strs.transaction)); - eb.WithDescription(new kwum(tr.Id).ToString()); - eb.AddField("Amount", N(tr.Amount)); - eb.AddField("Type", tr.Type, true); - eb.AddField("Extra", tr.Extra, true); - - if (tr.OtherId is ulong other) - { - eb.AddField("From Id", other); - } - - if (!string.IsNullOrWhiteSpace(tr.Note)) - { - eb.AddField("Note", tr.Note); - } - - eb.WithFooter(GetFormattedCurtrDate(tr)); - - await ctx.Channel.EmbedAsync(eb); - } - - private string GetHumanReadableTransaction(string type, string subType, ulong? maybeUserId) - => (type, subType, maybeUserId) switch - { - ("gift", var name, ulong userId) => GetText(strs.curtr_gift(name, userId)), - ("award", var name, ulong userId) => GetText(strs.curtr_award(name, userId)), - ("take", var name, ulong userId) => GetText(strs.curtr_take(name, userId)), - ("blackjack", _, _) => $"Blackjack - {subType}", - ("wheel", _, _) => $"Lucky Ladder - {subType}", - ("lula", _, _) => $"Lucky Ladder - {subType}", - ("rps", _, _) => $"Rock Paper Scissors - {subType}", - (null, _, _) => null, - (_, null, _) => null, - (_, _, ulong userId) => $"{type.Titleize()} - {subType.Titleize()} | [{userId}]", - _ => $"{type.Titleize()} - {subType.Titleize()}" - }; - - [Cmd] - [Priority(0)] - public async Task Cash(ulong userId) - { - var cur = await GetBalanceStringAsync(userId); - await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), cur)); - } - - private async Task BankAction(SocketMessageComponent smc, object _) - { - var balance = await _bank.GetBalanceAsync(ctx.User.Id); - - await N(balance) - .Pipe(strs.bank_balance) - .Pipe(GetText) - .Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true)); - } - - private EllieInteraction CreateCashInteraction() - => _inter.Create(ctx.User.Id, - new(new( - customId: "cash:bank_show_balance", - emote: new Emoji("🏦")), - BankAction)); - - [Cmd] - [Priority(1)] - public async Task Cash([Leftover] IUser user = null) - { - user ??= ctx.User; - var cur = await GetBalanceStringAsync(user.Id); - - var inter = user == ctx.User - ? CreateCashInteraction() - : null; - - await ConfirmLocalizedAsync( - user.ToString() - .Pipe(Format.Bold) - .With(cur) - .Pipe(strs.has), - inter); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task Give([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, IGuildUser receiver, [Leftover] string msg) - { - if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot) - { - return; - } - - if (!await _cs.TransferAsync(_eb, ctx.User, receiver, amount, msg, N(amount))) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - await ReplyConfirmLocalizedAsync(strs.gifted(N(amount), Format.Bold(receiver.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Give([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, [Leftover] IGuildUser receiver) - => Give(amount, receiver, null); - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(0)] - public Task Award(long amount, IGuildUser usr, [Leftover] string msg) - => Award(amount, usr.Id, msg); - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(1)] - public Task Award(long amount, [Leftover] IGuildUser usr) - => Award(amount, usr.Id); - - [Cmd] - [OwnerOnly] - [Priority(2)] - public async Task Award(long amount, ulong usrId, [Leftover] string msg = null) - { - if (amount <= 0) - { - return; - } - - var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId); - - if (usr is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found); - return; - } - - await _cs.AddAsync(usr.Id, amount, new("award", ctx.User.ToString()!, msg, ctx.User.Id)); - await ReplyConfirmLocalizedAsync(strs.awarded(N(amount), $"<@{usrId}>")); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(3)] - public async Task Award(long amount, [Leftover] IRole role) - { - var users = (await ctx.Guild.GetUsersAsync()).Where(u => u.GetRoles().Contains(role)).ToList(); - - await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(), - amount, - new("award", ctx.User.ToString()!, role.Name, ctx.User.Id)); - - await ReplyConfirmLocalizedAsync(strs.mass_award(N(amount), - Format.Bold(users.Count.ToString()), - Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(0)] - public async Task Take(long amount, [Leftover] IRole role) - { - var users = (await role.GetMembersAsync()).ToList(); - - await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(), - amount, - new("take", ctx.User.ToString()!, null, ctx.User.Id)); - - await ReplyConfirmLocalizedAsync(strs.mass_take(N(amount), - Format.Bold(users.Count.ToString()), - Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(1)] - public async Task Take(long amount, [Leftover] IGuildUser user) - { - if (amount <= 0) - { - return; - } - - var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id); - - if (await _cs.RemoveAsync(user.Id, amount, extra)) - { - await ReplyConfirmLocalizedAsync(strs.take(N(amount), Format.Bold(user.ToString()))); - } - else - { - await ReplyErrorLocalizedAsync(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign)); - } - } - - [Cmd] - [OwnerOnly] - public async Task Take(long amount, [Leftover] ulong usrId) - { - if (amount <= 0) - { - return; - } - - var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id); - - if (await _cs.RemoveAsync(usrId, amount, extra)) - { - await ReplyConfirmLocalizedAsync(strs.take(N(amount), $"<@{usrId}>")); - } - else - { - await ReplyErrorLocalizedAsync(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign)); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RollDuel(IUser u) - { - if (ctx.User.Id == u.Id) - { - return; - } - - //since the challenge is created by another user, we need to reverse the ids - //if it gets removed, means challenge is accepted - if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game)) - { - await game.StartGame(); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RollDuel([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, IUser u) - { - if (ctx.User.Id == u.Id) - { - return; - } - - if (amount <= 0) - { - return; - } - - var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.roll_duel)); - - var description = string.Empty; - - var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount); - //means challenge is just created - if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other)) - { - if (other.Amount != amount) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_already_challenged); - } - else - { - await RollDuel(u); - } - - return; - } - - if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game)) - { - game.OnGameTick += GameOnGameTick; - game.OnEnded += GameOnEnded; - - await ReplyConfirmLocalizedAsync(strs.roll_duel_challenge(Format.Bold(ctx.User.ToString()), - Format.Bold(u.ToString()), - Format.Bold(N(amount)))); - } - - async Task GameOnGameTick(RollDuelGame arg) - { - var rolls = arg.Rolls.Last(); - description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}** -{Format.Bold(u.ToString())} rolled **{rolls.Item2}** --- -"; - embed = embed.WithDescription(description); - - if (rdMsg is null) - { - rdMsg = await ctx.Channel.EmbedAsync(embed); - } - else - { - await rdMsg.ModifyAsync(x => { x.Embed = embed.Build(); }); - } - } - - async Task GameOnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason) - { - try - { - if (reason == RollDuelGame.Reason.Normal) - { - var winner = rdGame.Winner == rdGame.P1 ? ctx.User : u; - description += $"\n**{winner}** Won {N((long)(rdGame.Amount * 2 * 0.98))}"; - - embed = embed.WithDescription(description); - - await rdMsg.ModifyAsync(x => x.Embed = embed.Build()); - } - else if (reason == RollDuelGame.Reason.Timeout) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_timeout); - } - else if (reason == RollDuelGame.Reason.NoFunds) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_no_funds); - } - } - finally - { - _service.Duels.TryRemove((u.Id, ctx.User.Id), out _); - } - } - } - - [Cmd] - public async Task BetRoll([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - { - if (!await CheckBetMandatory(amount)) - { - return; - } - - var maybeResult = await _gs.BetRollAsync(ctx.User.Id, amount); - if (!maybeResult.TryPickT0(out var result, out _)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - - var win = (long)result.Won; - string str; - if (win > 0) - { - str = GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : ""))); - } - else - { - str = GetText(strs.better_luck); - } - - var eb = _eb.Create(ctx) - .WithAuthor(ctx.User) - .WithDescription(Format.Bold(str)) - .AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture)) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(eb); - } - - [Cmd] - [EllieOptions] - [Priority(0)] - public Task Leaderboard(params string[] args) - => Leaderboard(1, args); - - [Cmd] - [EllieOptions] - [Priority(1)] - public async Task Leaderboard(int page = 1, params string[] args) - { - if (--page < 0) - { - return; - } - - var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); - - List cleanRichest; - // it's pointless to have clean on dm context - if (ctx.Guild is null) - { - opts.Clean = false; - } - - if (opts.Clean) - { - await using (var uow = _db.GetDbContext()) - { - cleanRichest = uow.Set().GetTopRichest(_client.CurrentUser.Id, 10_000); - } - - await ctx.Channel.TriggerTypingAsync(); - await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); - - var sg = (SocketGuild)ctx.Guild!; - cleanRichest = cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList(); - } - else - { - await using var uow = _db.GetDbContext(); - cleanRichest = uow.Set().GetTopRichest(_client.CurrentUser.Id, 9, page).ToList(); - } - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var embed = _eb.Create().WithOkColor().WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); - - List toSend; - if (!opts.Clean) - { - using var uow = _db.GetDbContext(); - toSend = uow.Set().GetTopRichest(_client.CurrentUser.Id, 9, curPage); - } - else - { - toSend = cleanRichest.Skip(curPage * 9).Take(9).ToList(); - } - - if (!toSend.Any()) - { - embed.WithDescription(GetText(strs.no_user_on_this_page)); - return embed; - } - - for (var i = 0; i < toSend.Count; i++) - { - var x = toSend[i]; - var usrStr = x.ToString().TrimTo(20, true); - - var j = i; - embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true); - } - - return embed; - }, - opts.Clean ? cleanRichest.Count() : 9000, - 9, - opts.Clean); - } - - public enum InputRpsPick : byte - { - R = 0, - Rock = 0, - Rocket = 0, - P = 1, - Paper = 1, - Paperclip = 1, - S = 2, - Scissors = 2 - } - - [Cmd] - public async Task Rps(InputRpsPick pick, [OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default) - { - static string GetRpsPick(InputRpsPick p) - { - switch (p) - { - case InputRpsPick.R: - return "🚀"; - case InputRpsPick.P: - return "📎"; - default: - return "✂️"; - } - } - - if (!await CheckBetOptional(amount) || amount == 1) - return; - - var res = await _gs.RpsAsync(ctx.User.Id, amount, (byte)pick); - - if (!res.TryPickT0(out var result, out _)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var embed = _eb.Create(); - - string msg; - if (result.Result == RpsResultType.Draw) - { - msg = GetText(strs.rps_draw(GetRpsPick(pick))); - } - else if (result.Result == RpsResultType.Win) - { - if ((long)result.Won > 0) - embed.AddField(GetText(strs.won), N((long)result.Won)); - - msg = GetText(strs.rps_win(ctx.User.Mention, - GetRpsPick(pick), - GetRpsPick((InputRpsPick)result.ComputerPick))); - } - else - { - msg = GetText(strs.rps_win(ctx.Client.CurrentUser.Mention, - GetRpsPick((InputRpsPick)result.ComputerPick), - GetRpsPick(pick))); - } - - embed - .WithOkColor() - .WithDescription(msg); - - await ctx.Channel.EmbedAsync(embed); - } - - private static readonly ImmutableArray _emojis = - new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray(); - - [Cmd] - public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - { - if (!await CheckBetMandatory(amount)) - return; - - var res = await _gs.LulaAsync(ctx.User.Id, amount); - if (!res.TryPickT0(out var result, out _)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var multis = result.Multipliers; - - var sb = new StringBuilder(); - foreach (var multi in multis) - { - sb.Append($"╠══╣"); - - if (multi == result.Multiplier) - sb.Append($"{Format.Bold($"x{multi:0.##}")} ⬅️"); - else - sb.Append($"||x{multi:0.##}||"); - - sb.AppendLine(); - } - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithDescription(sb.ToString()) - .AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true) - .AddField(GetText(strs.won), $"{(long)result.Won}", true) - .WithAuthor(ctx.User); - - - await ctx.Channel.EmbedAsync(eb); - } - - - public enum GambleTestTarget - { - Slot, - Betroll, - Betflip, - BetflipT, - BetDraw, - BetDrawHL, - BetDrawRB, - Lula, - Rps, - } - - [Cmd] - [OwnerOnly] - public async Task BetTest() - { - var values = Enum.GetValues() - .Select(x => $"`{x}`") - .Join(", "); - - await SendConfirmAsync(GetText(strs.available_tests), values); - } - - [Cmd] - [OwnerOnly] - public async Task BetTest(GambleTestTarget target, int tests = 1000) - { - if (tests <= 0) - return; - - await ctx.Channel.TriggerTypingAsync(); - - var streak = 0; - var maxW = 0; - var maxL = 0; - - var dict = new Dictionary(); - for (var i = 0; i < tests; i++) - { - var multi = target switch - { - GambleTestTarget.BetDraw => (await _gs.BetDrawAsync(ctx.User.Id, 0, 1, 0)).AsT0.Multiplier, - GambleTestTarget.BetDrawRB => (await _gs.BetDrawAsync(ctx.User.Id, 0, null, 1)).AsT0.Multiplier, - GambleTestTarget.BetDrawHL => (await _gs.BetDrawAsync(ctx.User.Id, 0, 0, null)).AsT0.Multiplier, - GambleTestTarget.Slot => (await _gs.SlotAsync(ctx.User.Id, 0)).AsT0.Multiplier, - GambleTestTarget.Betflip => (await _gs.BetFlipAsync(ctx.User.Id, 0, 0)).AsT0.Multiplier, - GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier, - GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier, - GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier, - GambleTestTarget.Betroll => (await _gs.BetRollAsync(ctx.User.Id, 0)).AsT0.Multiplier, - _ => throw new ArgumentOutOfRangeException(nameof(target)) - }; - - if (dict.ContainsKey(multi)) - dict[multi] += 1; - else - dict.Add(multi, 1); - - if (multi < 1) - { - if (streak <= 0) - --streak; - else - streak = -1; - - maxL = Math.Max(maxL, -streak); - } - else if (multi > 1) - { - if (streak >= 0) - ++streak; - else - streak = 1; - - maxW = Math.Max(maxW, streak); - } - } - - var sb = new StringBuilder(); - decimal payout = 0; - foreach (var key in dict.Keys.OrderByDescending(x => x)) - { - sb.AppendLine($"x**{key}** occured `{dict[key]}` times. {dict[key] * 1.0f / tests * 100}%"); - payout += key * dict[key]; - } - - sb.AppendLine(); - sb.AppendLine($"Longest win streak: `{maxW}`"); - sb.AppendLine($"Longest lose streak: `{maxL}`"); - - await SendConfirmAsync(GetText(strs.test_results_for(target)), - sb.ToString(), - footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%"); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfig.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfig.cs deleted file mode 100644 index 76f25e1..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfig.cs +++ /dev/null @@ -1,387 +0,0 @@ -#nullable disable -using Cloneable; -using Ellie.Common.Yml; -using SixLabors.ImageSharp.PixelFormats; -using YamlDotNet.Serialization; -using Color = SixLabors.ImageSharp.Color; - -namespace Ellie.Modules.Gambling.Common; - -[Cloneable] -public sealed partial class GamblingConfig : ICloneable -{ - [Comment("""DO NOT CHANGE""")] - public int Version { get; set; } = 2; - - [Comment("""Currency settings""")] - public CurrencyConfig Currency { get; set; } - - [Comment("""Minimum amount users can bet (>=0)""")] - public int MinBet { get; set; } = 0; - - [Comment(""" - Maximum amount users can bet - Set 0 for unlimited - """)] - public int MaxBet { get; set; } = 0; - - [Comment("""Settings for betflip command""")] - public BetFlipConfig BetFlip { get; set; } - - [Comment("""Settings for betroll command""")] - public BetRollConfig BetRoll { get; set; } - - [Comment("""Automatic currency generation settings.""")] - public GenerationConfig Generation { get; set; } - - [Comment(""" - Settings for timely command - (letting people claim X amount of currency every Y hours) - """)] - public TimelyConfig Timely { get; set; } - - [Comment("""How much will each user's owned currency decay over time.""")] - public DecayConfig Decay { get; set; } - - [Comment("""Settings for LuckyLadder command""")] - public LuckyLadderSettings LuckyLadder { get; set; } - - [Comment("""Settings related to waifus""")] - public WaifuConfig Waifu { get; set; } - - [Comment(""" - Amount of currency selfhosters will get PER pledged dollar CENT. - 1 = 100 currency per $. Used almost exclusively on public nadeko. - """)] - public decimal PatreonCurrencyPerCent { get; set; } = 1; - - [Comment(""" - Currency reward per vote. - This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting - """)] - public long VoteReward { get; set; } = 100; - - [Comment("""Slot config""")] - public SlotsConfig Slots { get; set; } - - public GamblingConfig() - { - BetRoll = new(); - Waifu = new(); - Currency = new(); - BetFlip = new(); - Generation = new(); - Timely = new(); - Decay = new(); - Slots = new(); - LuckyLadder = new(); - } -} - -public class CurrencyConfig -{ - [Comment("""What is the emoji/character which represents the currency""")] - public string Sign { get; set; } = "🌸"; - - [Comment("""What is the name of the currency""")] - public string Name { get; set; } = "Nadeko Flower"; - - [Comment(""" - For how long (in days) will the transactions be kept in the database (curtrs) - Set 0 to disable cleanup (keep transactions forever) - """)] - public int TransactionsLifetime { get; set; } = 0; -} - -[Cloneable] -public partial class TimelyConfig -{ - [Comment(""" - How much currency will the users get every time they run .timely command - setting to 0 or less will disable this feature - """)] - public int Amount { get; set; } = 0; - - [Comment(""" - How often (in hours) can users claim currency with .timely command - setting to 0 or less will disable this feature - """)] - public int Cooldown { get; set; } = 24; -} - -[Cloneable] -public partial class BetFlipConfig -{ - [Comment("""Bet multiplier if user guesses correctly""")] - public decimal Multiplier { get; set; } = 1.95M; -} - -[Cloneable] -public partial class BetRollConfig -{ - [Comment(""" - When betroll is played, user will roll a number 0-100. - This setting will describe which multiplier is used for when the roll is higher than the given number. - Doesn't have to be ordered. - """)] - public BetRollPair[] Pairs { get; set; } = Array.Empty(); - - public BetRollConfig() - => Pairs = new BetRollPair[] - { - new() - { - WhenAbove = 99, - MultiplyBy = 10 - }, - new() - { - WhenAbove = 90, - MultiplyBy = 4 - }, - new() - { - WhenAbove = 66, - MultiplyBy = 2 - } - }; -} - -[Cloneable] -public partial class GenerationConfig -{ - [Comment(""" - when currency is generated, should it also have a random password - associated with it which users have to type after the .pick command - in order to get it - """)] - public bool HasPassword { get; set; } = true; - - [Comment(""" - Every message sent has a certain % chance to generate the currency - specify the percentage here (1 being 100%, 0 being 0% - for example - default is 0.02, which is 2% - """)] - public decimal Chance { get; set; } = 0.02M; - - [Comment("""How many seconds have to pass for the next message to have a chance to spawn currency""")] - public int GenCooldown { get; set; } = 10; - - [Comment("""Minimum amount of currency that can spawn""")] - public int MinAmount { get; set; } = 1; - - [Comment(""" - Maximum amount of currency that can spawn. - Set to the same value as MinAmount to always spawn the same amount - """)] - public int MaxAmount { get; set; } = 1; -} - -[Cloneable] -public partial class DecayConfig -{ - [Comment(""" - Percentage of user's current currency which will be deducted every 24h. - 0 - 1 (1 is 100%, 0.5 50%, 0 disabled) - """)] - public decimal Percent { get; set; } = 0; - - [Comment("""Maximum amount of user's currency that can decay at each interval. 0 for unlimited.""")] - public int MaxDecay { get; set; } = 0; - - [Comment("""Only users who have more than this amount will have their currency decay.""")] - public int MinThreshold { get; set; } = 99; - - [Comment("""How often, in hours, does the decay run. Default is 24 hours""")] - public int HourInterval { get; set; } = 24; -} - -[Cloneable] -public partial class LuckyLadderSettings -{ - [Comment("""Self-Explanatory. Has to have 8 values, otherwise the command won't work.""")] - public decimal[] Multipliers { get; set; } - - public LuckyLadderSettings() - => Multipliers = new[] { 2.4M, 1.7M, 1.5M, 1.2M, 0.5M, 0.3M, 0.2M, 0.1M }; -} - -[Cloneable] -public sealed partial class WaifuConfig -{ - [Comment("""Minimum price a waifu can have""")] - public long MinPrice { get; set; } = 50; - - public MultipliersData Multipliers { get; set; } = new(); - - [Comment(""" - Settings for periodic waifu price decay. - Waifu price decays only if the waifu has no claimer. - """)] - public WaifuDecayConfig Decay { get; set; } = new(); - - [Comment(""" - List of items available for gifting. - If negative is true, gift will instead reduce waifu value. - """)] - public List Items { get; set; } = new(); - - public WaifuConfig() - => Items = new() - { - new("🥔", 5, "Potato"), - new("🍪", 10, "Cookie"), - new("🥖", 20, "Bread"), - new("🍭", 30, "Lollipop"), - new("🌹", 50, "Rose"), - new("🍺", 70, "Beer"), - new("🌮", 85, "Taco"), - new("💌", 100, "LoveLetter"), - new("🥛", 125, "Milk"), - new("🍕", 150, "Pizza"), - new("🍫", 200, "Chocolate"), - new("🍦", 250, "Icecream"), - new("🍣", 300, "Sushi"), - new("🍚", 400, "Rice"), - new("🍉", 500, "Watermelon"), - new("🍱", 600, "Bento"), - new("🎟", 800, "MovieTicket"), - new("🍰", 1000, "Cake"), - new("📔", 1500, "Book"), - new("🐱", 2000, "Cat"), - new("🐶", 2001, "Dog"), - new("🐼", 2500, "Panda"), - new("💄", 3000, "Lipstick"), - new("👛", 3500, "Purse"), - new("📱", 4000, "iPhone"), - new("👗", 4500, "Dress"), - new("💻", 5000, "Laptop"), - new("🎻", 7500, "Violin"), - new("🎹", 8000, "Piano"), - new("🚗", 9000, "Car"), - new("💍", 10000, "Ring"), - new("🛳", 12000, "Ship"), - new("🏠", 15000, "House"), - new("🚁", 20000, "Helicopter"), - new("🚀", 30000, "Spaceship"), - new("🌕", 50000, "Moon") - }; - - public class WaifuDecayConfig - { - [Comment(""" - Percentage (0 - 100) of the waifu value to reduce. - Set 0 to disable - For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$) - """)] - public int Percent { get; set; } = 0; - - [Comment("""How often to decay waifu values, in hours""")] - public int HourInterval { get; set; } = 24; - - [Comment(""" - Minimum waifu price required for the decay to be applied. - For example if this value is set to 300, any waifu with the price 300 or less will not experience decay. - """)] - public long MinPrice { get; set; } = 300; - } -} - -[Cloneable] -public sealed partial class MultipliersData -{ - [Comment(""" - Multiplier for waifureset. Default 150. - Formula (at the time of writing this): - price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up - """)] - public int WaifuReset { get; set; } = 150; - - [Comment(""" - The minimum amount of currency that you have to pay - in order to buy a waifu who doesn't have a crush on you. - Default is 1.1 - Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her. - (100 * 1.1 = 110) - """)] - public decimal NormalClaim { get; set; } = 1.1m; - - [Comment(""" - The minimum amount of currency that you have to pay - in order to buy a waifu that has a crush on you. - Default is 0.88 - Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her. - (100 * 0.88 = 88) - """)] - public decimal CrushClaim { get; set; } = 0.88M; - - [Comment(""" - When divorcing a waifu, her new value will be her current value multiplied by this number. - Default 0.75 (meaning will lose 25% of her value) - """)] - public decimal DivorceNewValue { get; set; } = 0.75M; - - [Comment(""" - All gift prices will be multiplied by this number. - Default 1 (meaning no effect) - """)] - public decimal AllGiftPrices { get; set; } = 1.0M; - - [Comment(""" - What percentage of the value of the gift will a waifu gain when she's gifted. - Default 0.95 (meaning 95%) - Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095) - """)] - public decimal GiftEffect { get; set; } = 0.95M; - - [Comment(""" - What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'. - Default 0.5 (meaning 50%) - Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950) - """)] - public decimal NegativeGiftEffect { get; set; } = 0.50M; -} - -public sealed class SlotsConfig -{ - [Comment("""Hex value of the color which the numbers on the slot image will have.""")] - public Rgba32 CurrencyFontColor { get; set; } = Color.Red; -} - -[Cloneable] -public sealed partial class WaifuItemModel -{ - public string ItemEmoji { get; set; } - public long Price { get; set; } - public string Name { get; set; } - - [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] - public bool Negative { get; set; } - - public WaifuItemModel() - { - } - - public WaifuItemModel( - string itemEmoji, - long price, - string name, - bool negative = false) - { - ItemEmoji = itemEmoji; - Price = price; - Name = name; - Negative = negative; - } - - - public override string ToString() - => Name; -} - -[Cloneable] -public sealed partial class BetRollPair -{ - public int WhenAbove { get; set; } - public float MultiplyBy { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfigService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfigService.cs deleted file mode 100644 index f87e8c8..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingConfigService.cs +++ /dev/null @@ -1,186 +0,0 @@ -#nullable disable -using Ellie.Common.Configs; -using Ellie.Modules.Gambling.Common; - -namespace Ellie.Modules.Gambling.Services; - -public sealed class GamblingConfigService : ConfigServiceBase -{ - private const string FILE_PATH = "data/gambling.yml"; - private static readonly TypedKey _changeKey = new("config.gambling.updated"); - - public override string Name - => "gambling"; - - private readonly IEnumerable _antiGiftSeed = new[] - { - new WaifuItemModel("🥀", 100, "WiltedRose", true), new WaifuItemModel("✂️", 1000, "Haircut", true), - new WaifuItemModel("🧻", 10000, "ToiletPaper", true) - }; - - public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub) - : base(FILE_PATH, serializer, pubSub, _changeKey) - { - AddParsedProp("currency.name", - gs => gs.Currency.Name, - ConfigParsers.String, - ConfigPrinters.ToString); - - AddParsedProp("currency.sign", - gs => gs.Currency.Sign, - ConfigParsers.String, - ConfigPrinters.ToString); - - AddParsedProp("minbet", - gs => gs.MinBet, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("maxbet", - gs => gs.MaxBet, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("gen.min", - gs => gs.Generation.MinAmount, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 1); - - AddParsedProp("gen.max", - gs => gs.Generation.MaxAmount, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 1); - - AddParsedProp("gen.cd", - gs => gs.Generation.GenCooldown, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); - - AddParsedProp("gen.chance", - gs => gs.Generation.Chance, - decimal.TryParse, - ConfigPrinters.ToString, - val => val is >= 0 and <= 1); - - AddParsedProp("gen.has_pw", - gs => gs.Generation.HasPassword, - bool.TryParse, - ConfigPrinters.ToString); - - AddParsedProp("bf.multi", - gs => gs.BetFlip.Multiplier, - decimal.TryParse, - ConfigPrinters.ToString, - val => val >= 1); - - AddParsedProp("waifu.min_price", - gs => gs.Waifu.MinPrice, - long.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("waifu.multi.reset", - gs => gs.Waifu.Multipliers.WaifuReset, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("waifu.multi.crush_claim", - gs => gs.Waifu.Multipliers.CrushClaim, - decimal.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("waifu.multi.normal_claim", - gs => gs.Waifu.Multipliers.NormalClaim, - decimal.TryParse, - ConfigPrinters.ToString, - val => val > 0); - - AddParsedProp("waifu.multi.divorce_value", - gs => gs.Waifu.Multipliers.DivorceNewValue, - decimal.TryParse, - ConfigPrinters.ToString, - val => val > 0); - - AddParsedProp("waifu.multi.all_gifts", - gs => gs.Waifu.Multipliers.AllGiftPrices, - decimal.TryParse, - ConfigPrinters.ToString, - val => val > 0); - - AddParsedProp("waifu.multi.gift_effect", - gs => gs.Waifu.Multipliers.GiftEffect, - decimal.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("waifu.multi.negative_gift_effect", - gs => gs.Waifu.Multipliers.NegativeGiftEffect, - decimal.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("decay.percent", - gs => gs.Decay.Percent, - decimal.TryParse, - ConfigPrinters.ToString, - val => val is >= 0 and <= 1); - - AddParsedProp("decay.maxdecay", - gs => gs.Decay.MaxDecay, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("decay.threshold", - gs => gs.Decay.MinThreshold, - int.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - Migrate(); - } - - public void Migrate() - { - if (data.Version < 2) - { - ModifyConfig(c => - { - c.Waifu.Items = c.Waifu.Items.Concat(_antiGiftSeed).ToList(); - c.Version = 2; - }); - } - - if (data.Version < 3) - { - ModifyConfig(c => - { - c.Version = 3; - c.VoteReward = 100; - }); - } - - if (data.Version < 5) - { - ModifyConfig(c => - { - c.Version = 5; - }); - } - - if (data.Version < 6) - { - ModifyConfig(c => - { - c.Version = 6; - }); - } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingService.cs deleted file mode 100644 index 9ba2bac..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingService.cs +++ /dev/null @@ -1,221 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Connect4; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling.Services; - -public class GamblingService : IEService, IReadyExecutor -{ - public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new(); - public ConcurrentDictionary Connect4Games { get; } = new(); - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly IBotCache _cache; - private readonly GamblingConfigService _gss; - - private static readonly TypedKey _curDecayKey = new("currency:last_decay"); - - public GamblingService( - DbService db, - DiscordSocketClient client, - IBotCache cache, - GamblingConfigService gss) - { - _db = db; - _client = client; - _cache = cache; - _gss = gss; - } - - public Task OnReadyAsync() - => Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync()); - - private async Task TransactionClearLoopAsync() - { - if (_client.ShardId != 0) - return; - - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - while (await timer.WaitForNextTickAsync()) - { - try - { - var lifetime = _gss.Data.Currency.TransactionsLifetime; - if (lifetime <= 0) - continue; - - var now = DateTime.UtcNow; - var days = TimeSpan.FromDays(lifetime); - await using var uow = _db.GetDbContext(); - await uow.Set() - .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); - } - catch (Exception ex) - { - Log.Warning(ex, - "An unexpected error occurred in transactions cleanup loop: {ErrorMessage}", - ex.Message); - } - } - } - - private async Task CurrencyDecayLoopAsync() - { - if (_client.ShardId != 0) - return; - - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); - while (await timer.WaitForNextTickAsync()) - { - try - { - var config = _gss.Data; - var maxDecay = config.Decay.MaxDecay; - if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0) - continue; - - var now = DateTime.UtcNow; - - await using var uow = _db.GetDbContext(); - var result = await _cache.GetAsync(_curDecayKey); - - if (result.TryPickT0(out var bin, out _) - && (now - DateTime.FromBinary(bin) < TimeSpan.FromHours(config.Decay.HourInterval))) - { - continue; - } - - Log.Information(""" - --- Decaying users' currency --- - | decay: {ConfigDecayPercent}% - | max: {MaxDecay} - | threshold: {DecayMinTreshold} - """, - config.Decay.Percent * 100, - maxDecay, - config.Decay.MinThreshold); - - if (maxDecay == 0) - maxDecay = int.MaxValue; - - var decay = (double)config.Decay.Percent; - await uow.Set() - .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) - .UpdateAsync(old => new() - { - CurrencyAmount = - maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) - ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) - : old.CurrencyAmount - maxDecay - }); - - await uow.SaveChangesAsync(); - - await _cache.AddAsync(_curDecayKey, now.ToBinary()); - } - catch (Exception ex) - { - Log.Warning(ex, - "An unexpected error occurred in currency decay loop: {ErrorMessage}", - ex.Message); - } - } - } - - private static readonly TypedKey _ecoKey = new("nadeko:economy"); - - public async Task GetEconomyAsync() - { - var data = await _cache.GetOrAddAsync(_ecoKey, - async () => - { - await using var uow = _db.GetDbContext(); - var cash = uow.Set().GetTotalCurrency(); - var onePercent = uow.Set().GetTopOnePercentCurrency(_client.CurrentUser.Id); - decimal planted = uow.Set().AsQueryable().Sum(x => x.Amount); - var waifus = uow.Set().GetTotalValue(); - var bot = await uow.Set().GetUserCurrencyAsync(_client.CurrentUser.Id); - decimal bank = await uow.GetTable() - .SumAsyncLinqToDB(x => x.Balance); - - var result = new EconomyResult - { - Cash = cash, - Planted = planted, - Bot = bot, - Waifus = waifus, - OnePercent = onePercent, - Bank = bank - }; - - return result; - }, - TimeSpan.FromMinutes(3)); - - return data; - } - - - private static readonly SemaphoreSlim _timelyLock = new(1, 1); - - private static TypedKey> _timelyKey - = new("timely:claims"); - - public async Task ClaimTimelyAsync(ulong userId, int period) - { - if (period == 0) - return null; - - await _timelyLock.WaitAsync(); - try - { - // get the dictionary from the cache or get a new one - var dict = (await _cache.GetOrAddAsync(_timelyKey, - () => Task.FromResult(new Dictionary())))!; - - var now = DateTime.UtcNow; - var nowB = now.ToBinary(); - - // try to get users last claim - if (!dict.TryGetValue(userId, out var lastB)) - lastB = dict[userId] = now.ToBinary(); - - var diff = now - DateTime.FromBinary(lastB); - - // if its now, or too long ago => success - if (lastB == nowB || diff > period.Hours()) - { - // update the cache - dict[userId] = nowB; - await _cache.AddAsync(_timelyKey, dict); - - return null; - } - else - { - // otherwise return the remaining time - return period.Hours() - diff; - } - } - finally - { - _timelyLock.Release(); - } - } - - public bool UserHasTimelyReminder(ulong userId) - { - var db = _db.GetDbContext(); - return db.GetTable().Any(x => x.UserId == userId - && x.Type == ReminderType.Timely); - } - - public async Task RemoveAllTimelyClaimsAsync() - => await _cache.RemoveAsync(_timelyKey); -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs deleted file mode 100644 index d3e0192..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs +++ /dev/null @@ -1,68 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Services; -using System.Numerics; -using Ellie.Bot.Common; - -namespace Ellie.Modules.Gambling.Common; - -public abstract class GamblingModule : EllieModule -{ - protected GamblingConfig Config - => _lazyConfig.Value; - - protected string CurrencySign - => Config.Currency.Sign; - - protected string CurrencyName - => Config.Currency.Name; - - private readonly Lazy _lazyConfig; - - protected GamblingModule(GamblingConfigService gambService) - => _lazyConfig = new(() => gambService.Data); - - private async Task InternalCheckBet(long amount) - { - if (amount < 1) - return false; - if (amount < Config.MinBet) - { - await ReplyErrorLocalizedAsync(strs.min_bet_limit(Format.Bold(Config.MinBet.ToString()) + CurrencySign)); - return false; - } - - if (Config.MaxBet > 0 && amount > Config.MaxBet) - { - await ReplyErrorLocalizedAsync(strs.max_bet_limit(Format.Bold(Config.MaxBet.ToString()) + CurrencySign)); - return false; - } - - return true; - } - - protected string N(T cur) - where T : INumber - => CurrencyHelper.N(cur, Culture, CurrencySign); - - protected Task CheckBetMandatory(long amount) - { - if (amount < 1) - return Task.FromResult(false); - return InternalCheckBet(amount); - } - - protected Task CheckBetOptional(long amount) - { - if (amount == 0) - return Task.FromResult(true); - return InternalCheckBet(amount); - } -} - -public abstract class GamblingSubmodule : GamblingModule -{ - protected GamblingSubmodule(GamblingConfigService gamblingConfService) - : base(gamblingConfService) - { - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/GlobalUsings.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/GlobalUsings.cs deleted file mode 100644 index 1ba5b1d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/GlobalUsings.cs +++ /dev/null @@ -1,31 +0,0 @@ -// global using System.Collections.Generic -global using NonBlocking; - -// packages -global using Serilog; -global using Humanizer; - -// ellie -global using Ellie; -global using Ellie.Services; -global using Ellise.Common; // new project -global using Ellie.Common; // old + ellie specific things -global using Ellie.Common.Attributes; -global using Ellie.Extensions; -global using Ellie.Marmalade; - -// discord -global using Discord; -global using Discord.Commands; -global using Discord.Net; -global using Discord.WebSocket; - -// aliases -global using GuildPerm = Discord.GuildPermission; -global using ChannelPerm = Discord.ChannelPermission; -global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute; -global using LeftoverAttribute = Discord.Commands.RemainderAttribute; -global using TypeReaderResult = Ellie.Common.TypeReaders.TypeReaderResult; - -// non-essential -global using JetBrains.Annotations; diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/InputRpsPick.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/InputRpsPick.cs deleted file mode 100644 index 3cf537a..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/InputRpsPick.cs +++ /dev/null @@ -1,2 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling; \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantAndPickCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantAndPickCommands.cs deleted file mode 100644 index fc81124..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantAndPickCommands.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class PlantPickCommands : GamblingSubmodule - { - private readonly ILogCommandService _logService; - - public PlantPickCommands(ILogCommandService logService, GamblingConfigService gss) - : base(gss) - => _logService = logService; - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Pick(string pass = null) - { - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - return; - - var picked = await _service.PickAsync(ctx.Guild.Id, (ITextChannel)ctx.Channel, ctx.User.Id, pass); - - if (picked > 0) - { - var msg = await ReplyConfirmLocalizedAsync(strs.picked(N(picked))); - msg.DeleteAfter(10); - } - - if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) - { - try - { - _logService.AddDeleteIgnore(ctx.Message.Id); - await ctx.Message.DeleteAsync(); - } - catch { } - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Plant([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, string pass = null) - { - if (amount < 1) - return; - - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - return; - - if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) - { - _logService.AddDeleteIgnore(ctx.Message.Id); - await ctx.Message.DeleteAsync(); - } - - var success = await _service.PlantAsync(ctx.Guild.Id, - ctx.Channel, - ctx.User.Id, - ctx.User.ToString(), - amount, - pass); - - if (!success) - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] -#if GLOBAL_NADEKO - [OwnerOnly] -#endif - public async Task GenCurrency() - { - var enabled = _service.ToggleCurrencyGeneration(ctx.Guild.Id, ctx.Channel.Id); - if (enabled) - await ReplyConfirmLocalizedAsync(strs.curgen_enabled); - else - await ReplyConfirmLocalizedAsync(strs.curgen_disabled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [OwnerOnly] - public Task GenCurList(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - var enabledIn = _service.GetAllGeneratingChannels(); - - return ctx.SendPaginatedConfirmAsync(page, - _ => - { - var items = enabledIn.Skip(page * 9).Take(9).ToList(); - - if (!items.Any()) - return _eb.Create().WithErrorColor().WithDescription("-"); - - return items.Aggregate(_eb.Create().WithOkColor(), - (eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId)); - }, - enabledIn.Count(), - 9); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantPickService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantPickService.cs deleted file mode 100644 index 331e76b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/PlantPick/PlantPickService.cs +++ /dev/null @@ -1,385 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Services.Database.Models; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Color = SixLabors.ImageSharp.Color; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Gambling.Services; - -public class PlantPickService : IEService, IExecNoCommand -{ - //channelId/last generation - public ConcurrentDictionary LastGenerations { get; } = new(); - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly IImageCache _images; - private readonly FontProvider _fonts; - private readonly ICurrencyService _cs; - private readonly CommandHandler _cmdHandler; - private readonly EllieRandom _rng; - private readonly DiscordSocketClient _client; - private readonly GamblingConfigService _gss; - - private readonly ConcurrentHashSet _generationChannels; - private readonly SemaphoreSlim _pickLock = new(1, 1); - - public PlantPickService( - DbService db, - CommandHandler cmd, - IBotStrings strings, - IImageCache images, - FontProvider fonts, - ICurrencyService cs, - CommandHandler cmdHandler, - DiscordSocketClient client, - GamblingConfigService gss) - { - _db = db; - _strings = strings; - _images = images; - _fonts = fonts; - _cs = cs; - _cmdHandler = cmdHandler; - _rng = new(); - _client = client; - _gss = gss; - - using var uow = db.GetDbContext(); - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set() - .AsQueryable() - .Include(x => x.GenerateCurrencyChannelIds) - .Where(x => guildIds.Contains(x.GuildId)) - .ToList(); - - _generationChannels = new(configs.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); - } - - public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - => PotentialFlowerGeneration(msg); - - private string GetText(ulong gid, LocStr str) - => _strings.GetText(str, gid); - - public bool ToggleCurrencyGeneration(ulong gid, ulong cid) - { - bool enabled; - using var uow = _db.GetDbContext(); - var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); - - var toAdd = new GCChannelId - { - ChannelId = cid - }; - if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) - { - guildConfig.GenerateCurrencyChannelIds.Add(toAdd); - _generationChannels.Add(cid); - enabled = true; - } - else - { - var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); - if (toDelete is not null) - uow.Remove(toDelete); - _generationChannels.TryRemove(cid); - enabled = false; - } - - uow.SaveChanges(); - return enabled; - } - - public IEnumerable GetAllGeneratingChannels() - { - using var uow = _db.GetDbContext(); - var chs = uow.Set().GetGeneratingChannels(); - return chs; - } - - /// - /// Get a random currency image stream, with an optional password sticked onto it. - /// - /// Optional password to add to top left corner. - /// Extension of the file, defaults to png - /// Stream of the currency image - public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass) - { - var curImg = await _images.GetCurrencyImageAsync(); - - if (string.IsNullOrWhiteSpace(pass)) - { - // determine the extension - using var load = _ = Image.Load(curImg, out var format); - - // return the image - return (curImg.ToStream(), format.FileExtensions.FirstOrDefault() ?? "png"); - } - - // get the image stream and extension - return AddPassword(curImg, pass); - } - - /// - /// Add a password to the image. - /// - /// Image to add password to. - /// Password to add to top left corner. - /// Image with the password in the top left corner. - private (Stream, string) AddPassword(byte[] curImg, string pass) - { - // draw lower, it looks better - pass = pass.TrimTo(10, true).ToLowerInvariant(); - using var img = Image.Load(curImg, out var format); - // choose font size based on the image height, so that it's visible - var font = _fonts.NotoSans.CreateFont(img.Height / 12.0f, FontStyle.Bold); - img.Mutate(x => - { - // measure the size of the text to be drawing - var size = TextMeasurer.Measure(pass, new TextOptions(font) - { - Origin = new PointF(0, 0) - }); - - // fill the background with black, add 5 pixels on each side to make it look better - x.FillPolygon(Color.ParseHex("00000080"), - new PointF(0, 0), - new PointF(size.Width + 5, 0), - new PointF(size.Width + 5, size.Height + 10), - new PointF(0, size.Height + 10)); - - // draw the password over the background - x.DrawText(pass, font, Color.White, new(0, 0)); - }); - // return image as a stream for easy sending - return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png"); - } - - private Task PotentialFlowerGeneration(IUserMessage imsg) - { - if (imsg is not SocketUserMessage msg || msg.Author.IsBot) - return Task.CompletedTask; - - if (imsg.Channel is not ITextChannel channel) - return Task.CompletedTask; - - if (!_generationChannels.Contains(channel.Id)) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - try - { - var config = _gss.Data; - var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue.ToBinary()); - var rng = new EllieRandom(); - - if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) - < DateTime.FromBinary(lastGeneration)) //recently generated in this channel, don't generate again - return; - - var num = rng.Next(1, 101) + (config.Generation.Chance * 100); - if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow.ToBinary(), lastGeneration)) - { - var dropAmount = config.Generation.MinAmount; - var dropAmountMax = config.Generation.MaxAmount; - - if (dropAmountMax > dropAmount) - dropAmount = new EllieRandom().Next(dropAmount, dropAmountMax + 1); - - if (dropAmount > 0) - { - var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); - var toSend = dropAmount == 1 - ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign)) - + " " - + GetText(channel.GuildId, strs.pick_sn(prefix)) - : GetText(channel.GuildId, strs.curgen_pl(dropAmount, config.Currency.Sign)) - + " " - + GetText(channel.GuildId, strs.pick_pl(prefix)); - - var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; - - IUserMessage sent; - var (stream, ext) = await GetRandomCurrencyImageAsync(pw); - - await using (stream) - sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend); - - await AddPlantToDatabase(channel.GuildId, - channel.Id, - _client.CurrentUser.Id, - sent.Id, - dropAmount, - pw); - } - } - } - catch - { - } - }); - return Task.CompletedTask; - } - - /// - /// Generate a hexadecimal string from 1000 to ffff. - /// - /// A hexadecimal string from 1000 to ffff - private string GenerateCurrencyPassword() - { - // generate a number from 1000 to ffff - var num = _rng.Next(4096, 65536); - // convert it to hexadecimal - return num.ToString("x4"); - } - - public async Task PickAsync( - ulong gid, - ITextChannel ch, - ulong uid, - string pass) - { - await _pickLock.WaitAsync(); - try - { - long amount; - ulong[] ids; - await using (var uow = _db.GetDbContext()) - { - // this method will sum all plants with that password, - // remove them, and get messageids of the removed plants - - pass = pass?.Trim().TrimTo(10, true).ToUpperInvariant(); - // gets all plants in this channel with the same password - var entries = uow.Set().AsQueryable() - .Where(x => x.ChannelId == ch.Id && pass == x.Password) - .ToList(); - // sum how much currency that is, and get all of the message ids (so that i can delete them) - amount = entries.Sum(x => x.Amount); - ids = entries.Select(x => x.MessageId).ToArray(); - // remove them from the database - uow.RemoveRange(entries); - - - if (amount > 0) - // give the picked currency to the user - await _cs.AddAsync(uid, amount, new("currency", "collect")); - await uow.SaveChangesAsync(); - } - - try - { - // delete all of the plant messages which have just been picked - _ = ch.DeleteMessagesAsync(ids); - } - catch { } - - // return the amount of currency the user picked - return amount; - } - finally - { - _pickLock.Release(); - } - } - - public async Task SendPlantMessageAsync( - ulong gid, - IMessageChannel ch, - string user, - long amount, - string pass) - { - try - { - // get the text - var prefix = _cmdHandler.GetPrefix(gid); - var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + _gss.Data.Currency.Sign)); - - if (amount > 1) - msgToSend += " " + GetText(gid, strs.pick_pl(prefix)); - else - msgToSend += " " + GetText(gid, strs.pick_sn(prefix)); - - //get the image - var (stream, ext) = await GetRandomCurrencyImageAsync(pass); - // send it - await using (stream) - { - var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend); - // return sent message's id (in order to be able to delete it when it's picked) - return msg.Id; - } - } - catch (Exception ex) - { - // if sending fails, return null as message id - Log.Warning(ex, "Sending plant message failed: {Message}", ex.Message); - return null; - } - } - - public async Task PlantAsync( - ulong gid, - IMessageChannel ch, - ulong uid, - string user, - long amount, - string pass) - { - // normalize it - no more than 10 chars, uppercase - pass = pass?.Trim().TrimTo(10, true).ToUpperInvariant(); - // has to be either null or alphanumeric - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - return false; - - // remove currency from the user who's planting - if (await _cs.RemoveAsync(uid, amount, new("put/collect", "put"))) - { - // try to send the message with the currency image - var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass); - if (msgId is null) - { - // if it fails it will return null, if it returns null, refund - await _cs.AddAsync(uid, amount, new("put/collect", "refund")); - return false; - } - - // if it doesn't fail, put the plant in the database for other people to pick - await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass); - return true; - } - - // if user doesn't have enough currency, fail - return false; - } - - private async Task AddPlantToDatabase( - ulong gid, - ulong cid, - ulong uid, - ulong mid, - long amount, - string pass) - { - await using var uow = _db.GetDbContext(); - uow.Set().Add(new() - { - Amount = amount, - GuildId = gid, - ChannelId = cid, - Password = pass, - UserId = uid, - MessageId = mid - }); - await uow.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleCommands.cs deleted file mode 100644 index ff5d79e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleCommands.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - public partial class CurrencyRaffleCommands : GamblingSubmodule - { - public enum Mixed { Mixed } - - public CurrencyRaffleCommands(GamblingConfigService gamblingConfService) - : base(gamblingConfService) - { - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task RaffleCur(Mixed _, [OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - => RaffleCur(amount, true); - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task RaffleCur([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, bool mixed = false) - { - if (!await CheckBetMandatory(amount)) - return; - - async Task OnEnded(IUser arg, long won) - { - await SendConfirmAsync(GetText(strs.rafflecur_ended(CurrencyName, - Format.Bold(arg.ToString()), - won + CurrencySign))); - } - - var res = await _service.JoinOrCreateGame(ctx.Channel.Id, ctx.User, amount, mixed, OnEnded); - - if (res.Item1 is not null) - { - await SendConfirmAsync(GetText(strs.rafflecur(res.Item1.GameType.ToString())), - string.Join("\n", res.Item1.Users.Select(x => $"{x.DiscordUser} ({N(x.Amount)})")), - footer: GetText(strs.rafflecur_joined(ctx.User.ToString()))); - } - else - { - if (res.Item2 == CurrencyRaffleService.JoinErrorType.AlreadyJoinedOrInvalidAmount) - await ReplyErrorLocalizedAsync(strs.rafflecur_already_joined); - else if (res.Item2 == CurrencyRaffleService.JoinErrorType.NotEnoughCurrency) - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleGame.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleGame.cs deleted file mode 100644 index f730e2d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleGame.cs +++ /dev/null @@ -1,69 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common; - -public class CurrencyRaffleGame -{ - public enum Type - { - Mixed, - Normal - } - - public IEnumerable Users - => _users; - - public Type GameType { get; } - - private readonly HashSet _users = new(); - - public CurrencyRaffleGame(Type type) - => GameType = type; - - public bool AddUser(IUser usr, long amount) - { - // if game type is normal, and someone already joined the game - // (that's the user who created it) - if (GameType == Type.Normal && _users.Count > 0 && _users.First().Amount != amount) - return false; - - if (!_users.Add(new() - { - DiscordUser = usr, - Amount = amount - })) - return false; - - return true; - } - - public User GetWinner() - { - var rng = new EllieRandom(); - if (GameType == Type.Mixed) - { - var num = rng.NextLong(0L, Users.Sum(x => x.Amount)); - var sum = 0L; - foreach (var u in Users) - { - sum += u.Amount; - if (sum > num) - return u; - } - } - - var usrs = _users.ToArray(); - return usrs[rng.Next(0, usrs.Length)]; - } - - public class User - { - public IUser DiscordUser { get; set; } - public long Amount { get; set; } - - public override int GetHashCode() - => DiscordUser.GetHashCode(); - - public override bool Equals(object obj) - => obj is User u ? u.DiscordUser == DiscordUser : false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleService.cs deleted file mode 100644 index 31094fd..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Raffle/CurrencyRaffleService.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common; - -namespace Ellie.Modules.Gambling.Services; - -public class CurrencyRaffleService : IEService -{ - public enum JoinErrorType - { - NotEnoughCurrency, - AlreadyJoinedOrInvalidAmount - } - - public Dictionary Games { get; } = new(); - private readonly SemaphoreSlim _locker = new(1, 1); - private readonly ICurrencyService _cs; - - public CurrencyRaffleService(ICurrencyService cs) - => _cs = cs; - - public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame( - ulong channelId, - IUser user, - long amount, - bool mixed, - Func onEnded) - { - await _locker.WaitAsync(); - try - { - var newGame = false; - if (!Games.TryGetValue(channelId, out var crg)) - { - newGame = true; - crg = new(mixed ? CurrencyRaffleGame.Type.Mixed : CurrencyRaffleGame.Type.Normal); - Games.Add(channelId, crg); - } - - //remove money, and stop the game if this - // user created it and doesn't have the money - if (!await _cs.RemoveAsync(user.Id, amount, new("raffle", "join"))) - { - if (newGame) - Games.Remove(channelId); - return (null, JoinErrorType.NotEnoughCurrency); - } - - if (!crg.AddUser(user, amount)) - { - await _cs.AddAsync(user.Id, amount, new("raffle", "refund")); - return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount); - } - - if (newGame) - { - _ = Task.Run(async () => - { - await Task.Delay(60000); - await _locker.WaitAsync(); - try - { - var winner = crg.GetWinner(); - var won = crg.Users.Sum(x => x.Amount); - - await _cs.AddAsync(winner.DiscordUser.Id, won, new("raffle", "win")); - Games.Remove(channelId, out _); - _ = onEnded(winner.DiscordUser, won); - } - catch { } - finally { _locker.Release(); } - }); - } - - return (crg, null); - } - finally - { - _locker.Release(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/IShopService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/IShopService.cs deleted file mode 100644 index c3bddca..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/IShopService.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Services; - -public interface IShopService -{ - /// - /// Changes the price of a shop item - /// - /// Id of the guild in which the shop is - /// Index of the item - /// New item price - /// Success status - Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice); - - /// - /// Changes the name of a shop item - /// - /// Id of the guild in which the shop is - /// Index of the item - /// New item name - /// Success status - Task ChangeEntryNameAsync(ulong guildId, int index, string newName); - - /// - /// Swaps indexes of 2 items in the shop - /// - /// Id of the guild in which the shop is - /// First entry's index - /// Second entry's index - /// Whether swap was successful - Task SwapEntriesAsync(ulong guildId, int index1, int index2); - - /// - /// Swaps indexes of 2 items in the shop - /// - /// Id of the guild in which the shop is - /// Current index of the entry to move - /// Destination index of the entry - /// Whether swap was successful - Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex); - - Task SetItemRoleRequirementAsync(ulong guildId, int index, ulong? roleId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopCommands.cs deleted file mode 100644 index c953a14..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopCommands.cs +++ /dev/null @@ -1,496 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class ShopCommands : GamblingSubmodule - { - public enum List - { - List - } - - public enum Role - { - Role - } - - private readonly DbService _db; - private readonly ICurrencyService _cs; - - public ShopCommands(DbService db, ICurrencyService cs, GamblingConfigService gamblingConf) - : base(gamblingConf) - { - _db = db; - _cs = cs; - } - - private Task ShopInternalAsync(int page = 0) - { - if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); - - using var uow = _db.GetDbContext(); - var entries = uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)) - .ShopEntries.ToIndexed(); - return ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray(); - - if (!theseEntries.Any()) - return _eb.Create().WithErrorColor().WithDescription(GetText(strs.shop_none)); - var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.shop)); - - for (var i = 0; i < theseEntries.Length; i++) - { - var entry = theseEntries[i]; - embed.AddField($"#{(curPage * 9) + i + 1} - {N(entry.Price)}", - EntryToString(entry), - true); - } - - return embed; - }, - entries.Count, - 9); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Shop(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ShopInternalAsync(page); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Buy(int index) - { - index -= 1; - if (index < 0) - return; - ShopEntry entry; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)); - var entries = new IndexedCollection(config.ShopEntries); - entry = entries.ElementAtOrDefault(index); - uow.SaveChanges(); - } - - if (entry is null) - { - await ReplyErrorLocalizedAsync(strs.shop_item_not_found); - return; - } - - if (entry.RoleRequirement is ulong reqRoleId) - { - var role = ctx.Guild.GetRole(reqRoleId); - if (role is null) - { - await ReplyErrorLocalizedAsync(strs.shop_item_req_role_not_found); - return; - } - - var guser = (IGuildUser)ctx.User; - if (!guser.RoleIds.Contains(reqRoleId)) - { - await ReplyErrorLocalizedAsync(strs.shop_item_req_role_unfulfilled(Format.Bold(role.ToString()))); - return; - } - } - - if (entry.Type == ShopEntryType.Role) - { - var guser = (IGuildUser)ctx.User; - var role = ctx.Guild.GetRole(entry.RoleId); - - if (role is null) - { - await ReplyErrorLocalizedAsync(strs.shop_role_not_found); - return; - } - - if (guser.RoleIds.Any(id => id == role.Id)) - { - await ReplyErrorLocalizedAsync(strs.shop_role_already_bought); - return; - } - - if (await _cs.RemoveAsync(ctx.User.Id, entry.Price, new("shop", "buy", entry.Type.ToString()))) - { - try - { - await guser.AddRoleAsync(role); - } - catch (Exception ex) - { - Log.Warning(ex, "Error adding shop role"); - await _cs.AddAsync(ctx.User.Id, entry.Price, new("shop", "error-refund")); - await ReplyErrorLocalizedAsync(strs.shop_role_purchase_error); - return; - } - - var profit = GetProfitAmount(entry.Price); - await _cs.AddAsync(entry.AuthorId, profit, new("shop", "sell", $"Shop sell item - {entry.Type}")); - await _cs.AddAsync(ctx.Client.CurrentUser.Id, entry.Price - profit, new("shop", "cut")); - await ReplyConfirmLocalizedAsync(strs.shop_role_purchase(Format.Bold(role.Name))); - return; - } - - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - if (entry.Type == ShopEntryType.List) - { - if (entry.Items.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.out_of_stock); - return; - } - - var item = entry.Items.ToArray()[new EllieRandom().Next(0, entry.Items.Count)]; - - if (await _cs.RemoveAsync(ctx.User.Id, entry.Price, new("shop", "buy", entry.Type.ToString()))) - { - await using (var uow = _db.GetDbContext()) - { - uow.Set().Remove(item); - uow.SaveChanges(); - } - - try - { - await ctx.User.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.shop_purchase(ctx.Guild.Name))) - .AddField(GetText(strs.item), item.Text) - .AddField(GetText(strs.price), entry.Price.ToString(), true) - .AddField(GetText(strs.name), entry.Name, true)); - - await _cs.AddAsync(entry.AuthorId, - GetProfitAmount(entry.Price), - new("shop", "sell", entry.Name)); - } - catch - { - await _cs.AddAsync(ctx.User.Id, entry.Price, new("shop", "error-refund", entry.Name)); - await using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)) - .ShopEntries); - entry = entries.ElementAtOrDefault(index); - if (entry is not null) - { - if (entry.Items.Add(item)) - uow.SaveChanges(); - } - } - - await ReplyErrorLocalizedAsync(strs.shop_buy_error); - return; - } - - await ReplyConfirmLocalizedAsync(strs.shop_item_purchase); - } - else - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - } - - private static long GetProfitAmount(int price) - => (int)Math.Ceiling(0.90 * price); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ShopAdd(Role _, int price, [Leftover] IRole role) - { - if (price < 1) - return; - - var entry = new ShopEntry - { - Name = "-", - Price = price, - Type = ShopEntryType.Role, - AuthorId = ctx.User.Id, - RoleId = role.Id, - RoleName = role.Name - }; - await using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)) - .ShopEntries) - { - entry - }; - uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; - uow.SaveChanges(); - } - - await ctx.Channel.EmbedAsync(EntryToEmbed(entry).WithTitle(GetText(strs.shop_item_add))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopAdd(List _, int price, [Leftover] string name) - { - if (price < 1) - return; - - var entry = new ShopEntry - { - Name = name.TrimTo(100), - Price = price, - Type = ShopEntryType.List, - AuthorId = ctx.User.Id, - Items = new() - }; - await using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)) - .ShopEntries) - { - entry - }; - uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; - uow.SaveChanges(); - } - - await ctx.Channel.EmbedAsync(EntryToEmbed(entry).WithTitle(GetText(strs.shop_item_add))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopListAdd(int index, [Leftover] string itemText) - { - index -= 1; - if (index < 0) - return; - var item = new ShopEntryItem - { - Text = itemText - }; - ShopEntry entry; - var rightType = false; - var added = false; - await using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)) - .ShopEntries); - entry = entries.ElementAtOrDefault(index); - if (entry is not null && (rightType = entry.Type == ShopEntryType.List)) - { - if (entry.Items.Add(item)) - { - added = true; - uow.SaveChanges(); - } - } - } - - if (entry is null) - await ReplyErrorLocalizedAsync(strs.shop_item_not_found); - else if (!rightType) - await ReplyErrorLocalizedAsync(strs.shop_item_wrong_type); - else if (added == false) - await ReplyErrorLocalizedAsync(strs.shop_list_item_not_unique); - else - await ReplyConfirmLocalizedAsync(strs.shop_list_item_added); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopRemove(int index) - { - index -= 1; - if (index < 0) - return; - ShopEntry removed; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)); - - var entries = new IndexedCollection(config.ShopEntries); - removed = entries.ElementAtOrDefault(index); - if (removed is not null) - { - uow.RemoveRange(removed.Items); - uow.Remove(removed); - uow.SaveChanges(); - } - } - - if (removed is null) - await ReplyErrorLocalizedAsync(strs.shop_item_not_found); - else - await ctx.Channel.EmbedAsync(EntryToEmbed(removed).WithTitle(GetText(strs.shop_item_rm))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopChangePrice(int index, int price) - { - if (--index < 0 || price <= 0) - return; - - var succ = await _service.ChangeEntryPriceAsync(ctx.Guild.Id, index, price); - if (succ) - { - await ShopInternalAsync(index / 9); - await ctx.OkAsync(); - } - else - await ctx.ErrorAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopChangeName(int index, [Leftover] string newName) - { - if (--index < 0 || string.IsNullOrWhiteSpace(newName)) - return; - - var succ = await _service.ChangeEntryNameAsync(ctx.Guild.Id, index, newName); - if (succ) - { - await ShopInternalAsync(index / 9); - await ctx.OkAsync(); - } - else - await ctx.ErrorAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopSwap(int index1, int index2) - { - if (--index1 < 0 || --index2 < 0 || index1 == index2) - return; - - var succ = await _service.SwapEntriesAsync(ctx.Guild.Id, index1, index2); - if (succ) - { - await ShopInternalAsync(index1 / 9); - await ctx.OkAsync(); - } - else - await ctx.ErrorAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopMove(int fromIndex, int toIndex) - { - if (--fromIndex < 0 || --toIndex < 0 || fromIndex == toIndex) - return; - - var succ = await _service.MoveEntryAsync(ctx.Guild.Id, fromIndex, toIndex); - if (succ) - { - await ShopInternalAsync(toIndex / 9); - await ctx.OkAsync(); - } - else - await ctx.ErrorAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopReq(int itemIndex, [Leftover] IRole role = null) - { - if (--itemIndex < 0) - return; - - var succ = await _service.SetItemRoleRequirementAsync(ctx.Guild.Id, itemIndex, role?.Id); - if (!succ) - { - await ReplyErrorLocalizedAsync(strs.shop_item_not_found); - return; - } - - if (role is null) - await ReplyConfirmLocalizedAsync(strs.shop_item_role_no_req(itemIndex)); - else - await ReplyConfirmLocalizedAsync(strs.shop_item_role_req(itemIndex + 1, role)); - } - - public IEmbedBuilder EntryToEmbed(ShopEntry entry) - { - var embed = _eb.Create().WithOkColor(); - - if (entry.Type == ShopEntryType.Role) - { - return embed - .AddField(GetText(strs.name), - GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name - ?? "MISSING_ROLE"))), - true) - .AddField(GetText(strs.price), N(entry.Price), true) - .AddField(GetText(strs.type), entry.Type.ToString(), true); - } - - if (entry.Type == ShopEntryType.List) - { - return embed.AddField(GetText(strs.name), entry.Name, true) - .AddField(GetText(strs.price), N(entry.Price), true) - .AddField(GetText(strs.type), GetText(strs.random_unique_item), true); - } - - //else if (entry.Type == ShopEntryType.Infinite_List) - // return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(entry.RoleName)), true)) - // .AddField(GetText(strs.price), entry.Price.ToString(), true) - // .AddField(GetText(strs.type), entry.Type.ToString(), true); - return null; - } - - public string EntryToString(ShopEntry entry) - { - var prepend = string.Empty; - if (entry.RoleRequirement is not null) - prepend = Format.Italics(GetText(strs.shop_item_requires_role($"<@&{entry.RoleRequirement}>"))) - + Environment.NewLine; - - if (entry.Type == ShopEntryType.Role) - return prepend - + GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))); - if (entry.Type == ShopEntryType.List) - return prepend + GetText(strs.unique_items_left(entry.Items.Count)) + "\n" + entry.Name; - return prepend; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopService.cs deleted file mode 100644 index 91ed00d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Shop/ShopService.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling.Services; - -public class ShopService : IShopService, IEService -{ - private readonly DbService _db; - - public ShopService(DbService db) - => _db = db; - - private IndexedCollection GetEntriesInternal(DbContext uow, ulong guildId) - => uow.GuildConfigsForId(guildId, set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)) - .ShopEntries.ToIndexed(); - - public async Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (newPrice <= 0) - throw new ArgumentOutOfRangeException(nameof(newPrice)); - - await using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); - - if (index >= entries.Count) - return false; - - entries[index].Price = newPrice; - await uow.SaveChangesAsync(); - return true; - } - - public async Task ChangeEntryNameAsync(ulong guildId, int index, string newName) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (string.IsNullOrWhiteSpace(newName)) - throw new ArgumentNullException(nameof(newName)); - - await using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); - - if (index >= entries.Count) - return false; - - entries[index].Name = newName.TrimTo(100); - await uow.SaveChangesAsync(); - return true; - } - - public async Task SwapEntriesAsync(ulong guildId, int index1, int index2) - { - if (index1 < 0) - throw new ArgumentOutOfRangeException(nameof(index1)); - if (index2 < 0) - throw new ArgumentOutOfRangeException(nameof(index2)); - - await using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); - - if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2) - return false; - - entries[index1].Index = index2; - entries[index2].Index = index1; - - await uow.SaveChangesAsync(); - return true; - } - - public async Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex) - { - if (fromIndex < 0) - throw new ArgumentOutOfRangeException(nameof(fromIndex)); - if (toIndex < 0) - throw new ArgumentOutOfRangeException(nameof(toIndex)); - - await using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); - - if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex) - return false; - - var entry = entries[fromIndex]; - entries.RemoveAt(fromIndex); - entries.Insert(toIndex, entry); - - await uow.SaveChangesAsync(); - return true; - } - - public async Task SetItemRoleRequirementAsync(ulong guildId, int index, ulong? roleId) - { - await using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); - - if (index >= entries.Count) - return false; - - var entry = entries[index]; - - entry.RoleRequirement = roleId; - - await uow.SaveChangesAsync(); - return true; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Slot/SlotCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Slot/SlotCommands.cs deleted file mode 100644 index 3e360b5..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Slot/SlotCommands.cs +++ /dev/null @@ -1,227 +0,0 @@ -#nullable disable warnings -using Ellie.Db.Models; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Services; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Ellie.Econ.Gambling; -using Ellie.Common.TypeReaders; -using Color = SixLabors.ImageSharp.Color; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Gambling; - -public enum GamblingError -{ - InsufficientFunds, -} - -public partial class Gambling -{ - [Group] - public partial class SlotCommands : GamblingSubmodule - { - private static decimal totalBet; - private static decimal totalPaidOut; - - private readonly IImageCache _images; - private readonly FontProvider _fonts; - private readonly DbService _db; - private object _slotStatsLock = new(); - - public SlotCommands( - IImageCache images, - FontProvider fonts, - DbService db, - GamblingConfigService gamb) - : base(gamb) - { - _images = images; - _fonts = fonts; - _db = db; - } - - public Task Test() - => Task.CompletedTask; - - [Cmd] - public async Task Slot([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) - { - if (!await CheckBetMandatory(amount)) - return; - - // var slotInteraction = CreateSlotInteractionIntenal(amount); - - await ctx.Channel.TriggerTypingAsync(); - - if (await InternalSlotAsync(amount) is not SlotResult result) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var text = GetSlotMessageTextInternal(result); - - using var image = await GenerateSlotImageAsync(amount, result); - await using var imgStream = await image.ToStreamAsync(); - - - var eb = _eb.Create(ctx) - .WithAuthor(ctx.User) - .WithDescription(Format.Bold(text)) - .WithImageUrl($"attachment://result.png") - .WithOkColor(); - - var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again"); - var si = new SimpleInteraction(bb, (_, amount) => Slot(amount), amount); - - var inter = _inter.Create(ctx.User.Id, si); - var msg = await ctx.Channel.SendFileAsync(imgStream, - "result.png", - embed: eb.Build(), - components: inter.CreateComponent() - ); - await inter.RunAsync(msg); - } - - // private SlotInteraction CreateSlotInteractionIntenal(long amount) - // { - // return new SlotInteraction((DiscordSocketClient)ctx.Client, - // ctx.User.Id, - // async (smc) => - // { - // try - // { - // if (await InternalSlotAsync(amount) is not SlotResult result) - // { - // await smc.RespondErrorAsync(_eb, GetText(strs.not_enough(CurrencySign)), true); - // return; - // } - // - // var msg = GetSlotMessageInternal(result); - // - // using var image = await GenerateSlotImageAsync(amount, result); - // await using var imgStream = await image.ToStreamAsync(); - // - // var guid = Guid.NewGuid(); - // var imgName = $"result_{guid}.png"; - // - // var slotInteraction = CreateSlotInteractionIntenal(amount).GetInteraction(); - // - // await smc.Message.ModifyAsync(m => - // { - // m.Content = msg; - // m.Attachments = new[] - // { - // new FileAttachment(imgStream, imgName) - // }; - // m.Components = slotInteraction.CreateComponent(); - // }); - // - // _ = slotInteraction.RunAsync(smc.Message); - // } - // catch (Exception ex) - // { - // Log.Error(ex, "Error pulling slot again"); - // } - // // finally - // // { - // // await Task.Delay(1000); - // // _runningUsers.TryRemove(ctx.User.Id); - // // } - // }); - // } - - private string GetSlotMessageTextInternal(SlotResult result) - { - var multi = result.Multiplier.ToString("0.##"); - var msg = result.WinType switch - { - SlotWinType.SingleJoker => GetText(strs.slot_single(CurrencySign, multi)), - SlotWinType.DoubleJoker => GetText(strs.slot_two(CurrencySign, multi)), - SlotWinType.TrippleNormal => GetText(strs.slot_three(multi)), - SlotWinType.TrippleJoker => GetText(strs.slot_jackpot(multi)), - _ => GetText(strs.better_luck), - }; - return msg; - } - - private async Task InternalSlotAsync(long amount) - { - var maybeResult = await _service.SlotAsync(ctx.User.Id, amount); - - if (!maybeResult.TryPickT0(out var result, out var error)) - { - return null; - } - - lock (_slotStatsLock) - { - totalBet += amount; - totalPaidOut += result.Won; - } - - return result; - } - - private async Task> GenerateSlotImageAsync(long amount, SlotResult result) - { - long ownedAmount; - await using (var uow = _db.GetDbContext()) - { - ownedAmount = uow.Set().FirstOrDefault(x => x.UserId == ctx.User.Id)?.CurrencyAmount - ?? 0; - } - - var slotBg = await _images.GetSlotBgAsync(); - var bgImage = Image.Load(slotBg, out _); - var numbers = new int[3]; - result.Rolls.CopyTo(numbers, 0); - - Color fontColor = Config.Slots.CurrencyFontColor; - - bgImage.Mutate(x => x.DrawText(new TextOptions(_fonts.DottyFont.CreateFont(65)) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - WrappingLength = 140, - Origin = new(298, 100) - }, - ((long)result.Won).ToString(), - fontColor)); - - var bottomFont = _fonts.DottyFont.CreateFont(50); - - bgImage.Mutate(x => x.DrawText(new TextOptions(bottomFont) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - WrappingLength = 135, - Origin = new(196, 480) - }, - amount.ToString(), - fontColor)); - - bgImage.Mutate(x => x.DrawText(new(bottomFont) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - Origin = new(393, 480) - }, - ownedAmount.ToString(), - fontColor)); - //sw.PrintLap("drew red text"); - - for (var i = 0; i < 3; i++) - { - using var img = Image.Load(await _images.GetSlotEmojiAsync(numbers[i])); - bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f)); - } - - return bgImage; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/VoteRewardService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/VoteRewardService.cs deleted file mode 100644 index 21688ac..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/VoteRewardService.cs +++ /dev/null @@ -1,106 +0,0 @@ -#nullable disable -using Ellie.Common.ModuleBehaviors; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Gambling.Services; - -public class VoteModel -{ - [JsonPropertyName("userId")] - public ulong UserId { get; set; } -} - -public class VoteRewardService : IEService, IReadyExecutor -{ - private readonly DiscordSocketClient _client; - private readonly IBotCredentials _creds; - private readonly ICurrencyService _currencyService; - private readonly GamblingConfigService _gamb; - - public VoteRewardService( - DiscordSocketClient client, - IBotCredentials creds, - ICurrencyService currencyService, - GamblingConfigService gamb) - { - _client = client; - _creds = creds; - _currencyService = currencyService; - _gamb = gamb; - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var http = new HttpClient(new HttpClientHandler - { - AllowAutoRedirect = false, - ServerCertificateCustomValidationCallback = delegate { return true; } - }); - - while (true) - { - await Task.Delay(30000); - - var topggKey = _creds.Votes?.TopggKey; - var topggServiceUrl = _creds.Votes?.TopggServiceUrl; - - try - { - if (!string.IsNullOrWhiteSpace(topggKey) && !string.IsNullOrWhiteSpace(topggServiceUrl)) - { - http.DefaultRequestHeaders.Authorization = new(topggKey); - var uri = new Uri(new(topggServiceUrl), "topgg/new"); - var res = await http.GetStringAsync(uri); - var data = JsonSerializer.Deserialize>(res); - - if (data is { Count: > 0 }) - { - var ids = data.Select(x => x.UserId).ToList(); - - await _currencyService.AddBulkAsync(ids, - _gamb.Data.VoteReward, - new("vote", "top.gg", "top.gg vote reward")); - - Log.Information("Rewarding {Count} top.gg voters", ids.Count()); - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Critical error loading top.gg vote rewards"); - } - - var discordsKey = _creds.Votes?.DiscordsKey; - var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl; - - try - { - if (!string.IsNullOrWhiteSpace(discordsKey) && !string.IsNullOrWhiteSpace(discordsServiceUrl)) - { - http.DefaultRequestHeaders.Authorization = new(discordsKey); - var res = await http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new")); - var data = JsonSerializer.Deserialize>(res); - - if (data is { Count: > 0 }) - { - var ids = data.Select(x => x.UserId).ToList(); - - await _currencyService.AddBulkAsync(ids, - _gamb.Data.VoteReward, - new("vote", "discords", "discords.com vote reward")); - - Log.Information("Rewarding {Count} discords.com voters", ids.Count()); - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Critical error loading discords.com vote rewards"); - } - } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuClaimCommands.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuClaimCommands.cs deleted file mode 100644 index 3543231..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuClaimCommands.cs +++ /dev/null @@ -1,373 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Waifu; -using Ellie.Modules.Gambling.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling; - -public partial class Gambling -{ - [Group] - public partial class WaifuClaimCommands : GamblingSubmodule - { - public WaifuClaimCommands(GamblingConfigService gamblingConfService) - : base(gamblingConfService) - { - } - - [Cmd] - public async Task WaifuReset() - { - var price = _service.GetResetPrice(ctx.User); - var embed = _eb.Create() - .WithTitle(GetText(strs.waifu_reset_confirm)) - .WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price))))); - - if (!await PromptUserConfirmAsync(embed)) - return; - - if (await _service.TryReset(ctx.User)) - { - await ReplyConfirmLocalizedAsync(strs.waifu_reset); - return; - } - - await ReplyErrorLocalizedAsync(strs.waifu_reset_fail); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task WaifuClaim(long amount, [Leftover] IUser target) - { - if (amount < Config.Waifu.MinPrice) - { - await ReplyErrorLocalizedAsync(strs.waifu_isnt_cheap(Config.Waifu.MinPrice + CurrencySign)); - return; - } - - if (target.Id == ctx.User.Id) - { - await ReplyErrorLocalizedAsync(strs.waifu_not_yourself); - return; - } - - var (w, isAffinity, result) = await _service.ClaimWaifuAsync(ctx.User, target, amount); - - if (result == WaifuClaimResult.InsufficientAmount) - { - await ReplyErrorLocalizedAsync( - strs.waifu_not_enough(N((long)Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f))))); - return; - } - - if (result == WaifuClaimResult.NotEnoughFunds) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var msg = GetText(strs.waifu_claimed(Format.Bold(target.ToString()), N(amount))); - if (w.Affinity?.UserId == ctx.User.Id) - msg += "\n" + GetText(strs.waifu_fulfilled(target, N(w.Price))); - else - msg = " " + msg; - await SendConfirmAsync(ctx.User.Mention + msg); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task WaifuTransfer(ulong waifuId, IUser newOwner) - { - if (!await _service.WaifuTransfer(ctx.User, waifuId, newOwner)) - { - await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); - return; - } - - await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success(Format.Bold(waifuId.ToString()), - Format.Bold(ctx.User.ToString()), - Format.Bold(newOwner.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task WaifuTransfer(IUser waifu, IUser newOwner) - { - if (!await _service.WaifuTransfer(ctx.User, waifu.Id, newOwner)) - { - await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); - return; - } - - await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success(Format.Bold(waifu.ToString()), - Format.Bold(ctx.User.ToString()), - Format.Bold(newOwner.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(-1)] - public Task Divorce([Leftover] string target) - { - var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target); - if (waifuUserId == default) - return ReplyErrorLocalizedAsync(strs.waifu_not_yours); - - return Divorce(waifuUserId); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task Divorce([Leftover] IGuildUser target) - => Divorce(target.Id); - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task Divorce([Leftover] ulong targetId) - { - if (targetId == ctx.User.Id) - return; - - var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId); - - if (result == DivorceResult.SucessWithPenalty) - { - await ReplyConfirmLocalizedAsync(strs.waifu_divorced_like(Format.Bold(w.Waifu.ToString()), - N(amount))); - } - else if (result == DivorceResult.Success) - await ReplyConfirmLocalizedAsync(strs.waifu_divorced_notlike(N(amount))); - else if (result == DivorceResult.NotYourWife) - await ReplyErrorLocalizedAsync(strs.waifu_not_yours); - else - { - await ReplyErrorLocalizedAsync(strs.waifu_recent_divorce( - Format.Bold(((int)remaining?.TotalHours).ToString()), - Format.Bold(remaining?.Minutes.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Affinity([Leftover] IGuildUser user = null) - { - if (user?.Id == ctx.User.Id) - { - await ReplyErrorLocalizedAsync(strs.waifu_egomaniac); - return; - } - - var (oldAff, sucess, remaining) = await _service.ChangeAffinityAsync(ctx.User, user); - if (!sucess) - { - if (remaining is not null) - { - await ReplyErrorLocalizedAsync(strs.waifu_affinity_cooldown( - Format.Bold(((int)remaining?.TotalHours).ToString()), - Format.Bold(remaining?.Minutes.ToString()))); - } - else - await ReplyErrorLocalizedAsync(strs.waifu_affinity_already); - - return; - } - - if (user is null) - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_reset); - else if (oldAff is null) - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_set(Format.Bold(user.ToString()))); - else - { - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_changed(Format.Bold(oldAff.ToString()), - Format.Bold(user.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task WaifuLb(int page = 1) - { - page--; - - if (page < 0) - return; - - if (page > 100) - page = 100; - - var waifus = _service.GetTopWaifusAtPage(page).ToList(); - - if (waifus.Count == 0) - { - await ReplyConfirmLocalizedAsync(strs.waifus_none); - return; - } - - var embed = _eb.Create().WithTitle(GetText(strs.waifus_top_waifus)).WithOkColor(); - - var i = 0; - foreach (var w in waifus) - { - var j = i++; - embed.AddField("#" + ((page * 9) + j + 1) + " - " + N(w.Price), GetLbString(w)); - } - - await ctx.Channel.EmbedAsync(embed); - } - - private string GetLbString(WaifuLbResult w) - { - var claimer = "no one"; - var status = string.Empty; - - var waifuUsername = w.Username.TrimTo(20); - var claimerUsername = w.Claimer?.TrimTo(20); - - if (w.Claimer is not null) - claimer = $"{claimerUsername}#{w.ClaimerDiscrim}"; - if (w.Affinity is null) - status = $"... but {waifuUsername}'s heart is empty"; - else if (w.Affinity + w.AffinityDiscrim == w.Claimer + w.ClaimerDiscrim) - status = $"... and {waifuUsername} likes {claimerUsername} too <3"; - else - status = $"... but {waifuUsername}'s heart belongs to {w.Affinity.TrimTo(20)}#{w.AffinityDiscrim}"; - return $"**{waifuUsername}#{w.Discrim}** - claimed by **{claimer}**\n\t{status}"; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task WaifuInfo([Leftover] IUser target = null) - { - if (target is null) - target = ctx.User; - - return InternalWaifuInfo(target.Id, target.ToString()); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task WaifuInfo(ulong targetId) - => InternalWaifuInfo(targetId); - - private async Task InternalWaifuInfo(ulong targetId, string name = null) - { - var wi = await _service.GetFullWaifuInfoAsync(targetId); - var affInfo = _service.GetAffinityTitle(wi.AffinityCount); - - var waifuItems = _service.GetWaifuItems().ToDictionary(x => x.ItemEmoji, x => x); - - var nobody = GetText(strs.nobody); - var itemList = await _service.GetItems(wi.WaifuId); - var itemsStr = !itemList.Any() - ? "-" - : string.Join("\n", - itemList.Where(x => waifuItems.TryGetValue(x.ItemEmoji, out _)) - .OrderBy(x => waifuItems[x.ItemEmoji].Price) - .GroupBy(x => x.ItemEmoji) - .Select(x => $"{x.Key} x{x.Count(),-3}") - .Chunk(2) - .Select(x => string.Join(" ", x))); - - var claimsNames = (await _service.GetClaimNames(wi.WaifuId)); - var claimsStr = claimsNames - .Shuffle() - .Take(30) - .Join('\n'); - - var fansList = await _service.GetFansNames(wi.WaifuId); - var fansStr = fansList - .Select((x) => claimsNames.Contains(x) ? $"{x} 💞" : x) - .Join('\n'); - - - if (string.IsNullOrWhiteSpace(fansStr)) - fansStr = "-"; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.waifu) - + " " - + (wi.FullName ?? name ?? targetId.ToString()) - + " - \"the " - + _service.GetClaimTitle(wi.ClaimCount) - + "\"") - .AddField(GetText(strs.price), N(wi.Price), true) - .AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true) - .AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true) - .AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true) - .AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true) - .AddField("\u200B", "\u200B", true) - .AddField(GetText(strs.fans(fansList.Count)), fansStr, true) - .AddField($"Waifus ({wi.ClaimCount})", - wi.ClaimCount == 0 ? nobody : claimsStr, - true) - .AddField(GetText(strs.gifts), itemsStr, true); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task WaifuGift(int page = 1) - { - if (--page < 0 || page > (Config.Waifu.Items.Count - 1) / 9) - return; - - var waifuItems = _service.GetWaifuItems(); - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var embed = _eb.Create().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor(); - - waifuItems.OrderBy(x => x.Negative) - .ThenBy(x => x.Price) - .Skip(9 * cur) - .Take(9) - .ToList() - .ForEach(x => embed.AddField( - $"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}", - Format.Bold(N(x.Price)), - true)); - - return embed; - }, - waifuItems.Count, - 9); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task WaifuGift(string itemName, [Leftover] IUser waifu) - { - if (waifu.Id == ctx.User.Id) - return; - - var allItems = _service.GetWaifuItems(); - var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant()); - if (item is null) - { - await ReplyErrorLocalizedAsync(strs.waifu_gift_not_exist); - return; - } - - var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item); - - if (sucess) - { - await ReplyConfirmLocalizedAsync(strs.waifu_gift(Format.Bold(item + " " + item.ItemEmoji), - Format.Bold(waifu.ToString()))); - } - else - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuService.cs deleted file mode 100644 index 8341033..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/WaifuService.cs +++ /dev/null @@ -1,583 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Gambling.Common; -using Ellie.Modules.Gambling.Common.Waifu; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Gambling.Services; - -public class WaifuService : IEService, IReadyExecutor -{ - private readonly DbService _db; - private readonly ICurrencyService _cs; - private readonly IBotCache _cache; - private readonly GamblingConfigService _gss; - private readonly IBotCredentials _creds; - private readonly DiscordSocketClient _client; - - public WaifuService( - DbService db, - ICurrencyService cs, - IBotCache cache, - GamblingConfigService gss, - IBotCredentials creds, - DiscordSocketClient client) - { - _db = db; - _cs = cs; - _cache = cache; - _gss = gss; - _creds = creds; - _client = client; - } - - public async Task WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner) - { - if (owner.Id == newOwner.Id || waifuId == newOwner.Id) - return false; - - var settings = _gss.Data; - - await using var uow = _db.GetDbContext(); - var waifu = uow.Set().ByWaifuUserId(waifuId); - var ownerUser = uow.GetOrCreateUser(owner); - - // owner has to be the owner of the waifu - if (waifu is null || waifu.ClaimerId != ownerUser.Id) - return false; - - // if waifu likes the person, gotta pay the penalty - if (waifu.AffinityId == ownerUser.Id) - { - if (!await _cs.RemoveAsync(owner.Id, (long)(waifu.Price * 0.6), new("waifu", "affinity-penalty"))) - // unable to pay 60% penalty - return false; - - waifu.Price = (long)(waifu.Price * 0.7); // half of 60% = 30% price reduction - if (waifu.Price < settings.Waifu.MinPrice) - waifu.Price = settings.Waifu.MinPrice; - } - else // if not, pay 10% fee - { - if (!await _cs.RemoveAsync(owner.Id, waifu.Price / 10, new("waifu", "transfer"))) - return false; - - waifu.Price = (long)(waifu.Price * 0.95); // half of 10% = 5% price reduction - if (waifu.Price < settings.Waifu.MinPrice) - waifu.Price = settings.Waifu.MinPrice; - } - - //new claimerId is the id of the new owner - var newOwnerUser = uow.GetOrCreateUser(newOwner); - waifu.ClaimerId = newOwnerUser.Id; - - await uow.SaveChangesAsync(); - - return true; - } - - public long GetResetPrice(IUser user) - { - var settings = _gss.Data; - using var uow = _db.GetDbContext(); - var waifu = uow.Set().ByWaifuUserId(user.Id); - - if (waifu is null) - return settings.Waifu.MinPrice; - - var divorces = uow.Set().Count(x - => x.Old != null && x.Old.UserId == user.Id && x.UpdateType == WaifuUpdateType.Claimed && x.New == null); - var affs = uow.Set().AsQueryable() - .Where(w => w.User.UserId == user.Id - && w.UpdateType == WaifuUpdateType.AffinityChanged - && w.New != null) - .ToList() - .GroupBy(x => x.New) - .Count(); - - return (long)Math.Ceiling(waifu.Price * 1.25f) - + ((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset); - } - - public async Task TryReset(IUser user) - { - await using var uow = _db.GetDbContext(); - var price = GetResetPrice(user); - if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset"))) - return false; - - var affs = uow.Set().AsQueryable() - .Where(w => w.User.UserId == user.Id - && w.UpdateType == WaifuUpdateType.AffinityChanged - && w.New != null); - - var divorces = uow.Set().AsQueryable() - .Where(x => x.Old != null - && x.Old.UserId == user.Id - && x.UpdateType == WaifuUpdateType.Claimed - && x.New == null); - - //reset changes of heart to 0 - uow.Set().RemoveRange(affs); - //reset divorces to 0 - uow.Set().RemoveRange(divorces); - var waifu = uow.Set().ByWaifuUserId(user.Id); - //reset price, remove items - //remove owner, remove affinity - waifu.Price = 50; - waifu.Items.Clear(); - waifu.ClaimerId = null; - waifu.AffinityId = null; - - //wives stay though - - await uow.SaveChangesAsync(); - - return true; - } - - public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, long amount) - { - var settings = _gss.Data; - WaifuClaimResult result; - WaifuInfo w; - bool isAffinity; - await using (var uow = _db.GetDbContext()) - { - w = uow.Set().ByWaifuUserId(target.Id); - isAffinity = w?.Affinity?.UserId == user.Id; - if (w is null) - { - var claimer = uow.GetOrCreateUser(user); - var waifu = uow.GetOrCreateUser(target); - if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim"))) - result = WaifuClaimResult.NotEnoughFunds; - else - { - uow.Set().Add(w = new() - { - Waifu = waifu, - Claimer = claimer, - Affinity = null, - Price = amount - }); - uow.Set().Add(new() - { - User = waifu, - Old = null, - New = claimer, - UpdateType = WaifuUpdateType.Claimed - }); - result = WaifuClaimResult.Success; - } - } - else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim) - { - if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim"))) - result = WaifuClaimResult.NotEnoughFunds; - else - { - var oldClaimer = w.Claimer; - w.Claimer = uow.GetOrCreateUser(user); - w.Price = amount + (amount / 4); - result = WaifuClaimResult.Success; - - uow.Set().Add(new() - { - User = w.Waifu, - Old = oldClaimer, - New = w.Claimer, - UpdateType = WaifuUpdateType.Claimed - }); - } - } - else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity - { - if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim"))) - result = WaifuClaimResult.NotEnoughFunds; - else - { - var oldClaimer = w.Claimer; - w.Claimer = uow.GetOrCreateUser(user); - w.Price = amount; - result = WaifuClaimResult.Success; - - uow.Set().Add(new() - { - User = w.Waifu, - Old = oldClaimer, - New = w.Claimer, - UpdateType = WaifuUpdateType.Claimed - }); - } - } - else - result = WaifuClaimResult.InsufficientAmount; - - - await uow.SaveChangesAsync(); - } - - return (w, isAffinity, result); - } - - public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target) - { - DiscordUser oldAff = null; - var success = false; - TimeSpan? remaining = null; - await using (var uow = _db.GetDbContext()) - { - var w = uow.Set().ByWaifuUserId(user.Id); - var newAff = target is null ? null : uow.GetOrCreateUser(target); - if (w?.Affinity?.UserId == target?.Id) - { - return (null, false, null); - } - - remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id), - 30.Minutes()); - - if (remaining is not null) - { - } - else if (w is null) - { - var thisUser = uow.GetOrCreateUser(user); - uow.Set().Add(new() - { - Affinity = newAff, - Waifu = thisUser, - Price = 1, - Claimer = null - }); - success = true; - - uow.Set().Add(new() - { - User = thisUser, - Old = null, - New = newAff, - UpdateType = WaifuUpdateType.AffinityChanged - }); - } - else - { - if (w.Affinity is not null) - oldAff = w.Affinity; - w.Affinity = newAff; - success = true; - - uow.Set().Add(new() - { - User = w.Waifu, - Old = oldAff, - New = newAff, - UpdateType = WaifuUpdateType.AffinityChanged - }); - } - - await uow.SaveChangesAsync(); - } - - return (oldAff, success, remaining); - } - - public IEnumerable GetTopWaifusAtPage(int page) - { - using var uow = _db.GetDbContext(); - return uow.Set().GetTop(9, page * 9); - } - - public ulong GetWaifuUserId(ulong ownerId, string name) - { - using var uow = _db.GetDbContext(); - return uow.Set().GetWaifuUserId(ownerId, name); - } - - private static TypedKey GetDivorceKey(ulong userId) - => new($"waifu:divorce_cd:{userId}"); - - private static TypedKey GetAffinityKey(ulong userId) - => new($"waifu:affinity:{userId}"); - - public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId) - { - DivorceResult result; - TimeSpan? remaining = null; - long amount = 0; - WaifuInfo w; - await using (var uow = _db.GetDbContext()) - { - w = uow.Set().ByWaifuUserId(targetId); - if (w?.Claimer is null || w.Claimer.UserId != user.Id) - result = DivorceResult.NotYourWife; - else - { - remaining = await _cache.GetRatelimitAsync(GetDivorceKey(user.Id), 6.Hours()); - if (remaining is TimeSpan rem) - { - result = DivorceResult.Cooldown; - return (w, result, amount, rem); - } - - amount = w.Price / 2; - - if (w.Affinity?.UserId == user.Id) - { - await _cs.AddAsync(w.Waifu.UserId, amount, new("waifu", "compensation")); - w.Price = (long)Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue); - result = DivorceResult.SucessWithPenalty; - } - else - { - await _cs.AddAsync(user.Id, amount, new("waifu", "refund")); - - result = DivorceResult.Success; - } - - var oldClaimer = w.Claimer; - w.Claimer = null; - - uow.Set().Add(new() - { - User = w.Waifu, - Old = oldClaimer, - New = null, - UpdateType = WaifuUpdateType.Claimed - }); - } - - await uow.SaveChangesAsync(); - } - - return (w, result, amount, remaining); - } - - public async Task GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj) - { - if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item"))) - return false; - - await using var uow = _db.GetDbContext(); - var w = uow.Set().ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer)); - if (w is null) - { - uow.Set().Add(w = new() - { - Affinity = null, - Claimer = null, - Price = 1, - Waifu = uow.GetOrCreateUser(giftedWaifu) - }); - } - - if (!itemObj.Negative) - { - w.Items.Add(new() - { - Name = itemObj.Name.ToLowerInvariant(), - ItemEmoji = itemObj.ItemEmoji - }); - - if (w.Claimer?.UserId == from.Id) - w.Price += (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); - else - w.Price += itemObj.Price / 2; - } - else - { - w.Price -= (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect); - if (w.Price < 1) - w.Price = 1; - } - - await uow.SaveChangesAsync(); - - return true; - } - - public async Task GetFullWaifuInfoAsync(ulong targetId) - { - await using var uow = _db.GetDbContext(); - var wi = await uow.GetWaifuInfoAsync(targetId); - if (wi is null) - { - wi = new() - { - AffinityCount = 0, - AffinityName = null, - ClaimCount = 0, - ClaimerName = null, - DivorceCount = 0, - FullName = null, - Price = 1 - }; - } - - return wi; - } - - public string GetClaimTitle(int count) - { - ClaimTitle title; - if (count == 0) - title = ClaimTitle.Lonely; - else if (count == 1) - title = ClaimTitle.Devoted; - else if (count < 3) - title = ClaimTitle.Rookie; - else if (count < 6) - title = ClaimTitle.Schemer; - else if (count < 10) - title = ClaimTitle.Dilettante; - else if (count < 17) - title = ClaimTitle.Intermediate; - else if (count < 25) - title = ClaimTitle.Seducer; - else if (count < 35) - title = ClaimTitle.Expert; - else if (count < 50) - title = ClaimTitle.Veteran; - else if (count < 75) - title = ClaimTitle.Incubis; - else if (count < 100) - title = ClaimTitle.Harem_King; - else - title = ClaimTitle.Harem_God; - - return title.ToString().Replace('_', ' '); - } - - public string GetAffinityTitle(int count) - { - AffinityTitle title; - if (count < 1) - title = AffinityTitle.Pure; - else if (count < 2) - title = AffinityTitle.Faithful; - else if (count < 4) - title = AffinityTitle.Playful; - else if (count < 8) - title = AffinityTitle.Cheater; - else if (count < 11) - title = AffinityTitle.Tainted; - else if (count < 15) - title = AffinityTitle.Corrupted; - else if (count < 20) - title = AffinityTitle.Lewd; - else if (count < 25) - title = AffinityTitle.Sloot; - else if (count < 35) - title = AffinityTitle.Depraved; - else - title = AffinityTitle.Harlot; - - return title.ToString().Replace('_', ' '); - } - - public IReadOnlyList GetWaifuItems() - { - var conf = _gss.Data; - return conf.Waifu.Items.Select(x - => new WaifuItemModel(x.ItemEmoji, - (long)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), - x.Name, - x.Negative)) - .ToList(); - } - - private static readonly TypedKey _waifuDecayKey = $"waifu:last_decay"; - public async Task OnReadyAsync() - { - // only decay waifu values from shard 0 - if (_client.ShardId != 0) - return; - - while (true) - { - try - { - var multi = _gss.Data.Waifu.Decay.Percent / 100f; - var minPrice = _gss.Data.Waifu.Decay.MinPrice; - var decayInterval = _gss.Data.Waifu.Decay.HourInterval; - - if (multi is < 0f or > 1f || decayInterval < 0) - continue; - - var now = DateTime.UtcNow; - var nowB = now.ToBinary(); - - var result = await _cache.GetAsync(_waifuDecayKey); - - if (result.TryGetValue(out var val)) - { - var lastDecay = DateTime.FromBinary(val); - var toWait = decayInterval.Hours() - (DateTime.UtcNow - lastDecay); - - if (toWait > 0.Hours()) - continue; - } - - await _cache.AddAsync(_waifuDecayKey, nowB); - - await using var uow = _db.GetDbContext(); - - await uow.GetTable() - .Where(x => x.Price > minPrice && x.ClaimerId == null) - .UpdateAsync(old => new() - { - Price = (long)(old.Price * multi) - }); - - } - catch (Exception ex) - { - Log.Error(ex, "Unexpected error occured in waifu decay loop: {ErrorMessage}", ex.Message); - } - finally - { - await Task.Delay(1.Hours()); - } - } - } - - public async Task> GetClaimNames(int waifuId) - { - await using var ctx = _db.GetDbContext(); - return await ctx.GetTable() - .Where(x => ctx.GetTable() - .Where(wi => wi.ClaimerId == waifuId) - .Select(wi => wi.WaifuId) - .Contains(x.Id)) - .Select(x => $"{x.Username}#{x.Discriminator}") - .ToListAsyncEF(); - } - public async Task> GetFansNames(int waifuId) - { - await using var ctx = _db.GetDbContext(); - return await ctx.GetTable() - .Where(x => ctx.GetTable() - .Where(wi => wi.AffinityId == waifuId) - .Select(wi => wi.WaifuId) - .Contains(x.Id)) - .Select(x => $"{x.Username}#{x.Discriminator}") - .ToListAsyncEF(); - } - - public async Task> GetItems(int waifuId) - { - await using var ctx = _db.GetDbContext(); - return await ctx.GetTable() - .Where(x => x.WaifuInfoId == ctx.GetTable() - .Where(x => x.WaifuId == waifuId) - .Select(x => x.Id) - .FirstOrDefault()) - .ToListAsyncEF(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/AffinityTitle.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/AffinityTitle.cs deleted file mode 100644 index 090def6..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/AffinityTitle.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.Waifu; - -public enum AffinityTitle -{ - Pure, - Faithful, - Playful, - Cheater, - Tainted, - Corrupted, - Lewd, - Sloot, - Depraved, - Harlot -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/ClaimTitle.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/ClaimTitle.cs deleted file mode 100644 index 981de85..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/ClaimTitle.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.Waifu; - -public enum ClaimTitle -{ - Lonely, - Devoted, - Rookie, - Schemer, - Dilettante, - Intermediate, - Seducer, - Expert, - Veteran, - Incubis, - Harem_King, - Harem_God -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/DivorceResult.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/DivorceResult.cs deleted file mode 100644 index 44b3b0a..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/DivorceResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.Waifu; - -public enum DivorceResult -{ - Success, - SucessWithPenalty, - NotYourWife, - Cooldown -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/Extensions.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/Extensions.cs deleted file mode 100644 index 62d3850..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/Extensions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Modules.Gambling.Common.Waifu; - -public class Extensions -{ - -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/WaifuClaimResult.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/WaifuClaimResult.cs deleted file mode 100644 index 0513ae1..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/_common/WaifuClaimResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common.Waifu; - -public enum WaifuClaimResult -{ - Success, - NotEnoughFunds, - InsufficientAmount -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/Waifu.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/Waifu.cs deleted file mode 100644 index 182c4d9..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/Waifu.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using Ellie.Db.Models; - -namespace Ellie.Services.Database.Models; - -public class WaifuInfo : DbEntity -{ - public int WaifuId { get; set; } - public DiscordUser Waifu { get; set; } - - public int? ClaimerId { get; set; } - public DiscordUser Claimer { get; set; } - - public int? AffinityId { get; set; } - public DiscordUser Affinity { get; set; } - - public long Price { get; set; } - public List Items { get; set; } = new(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuExtensions.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuExtensions.cs deleted file mode 100644 index 4112430..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuExtensions.cs +++ /dev/null @@ -1,133 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Db.Models; -using Ellie.Services.Database; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class WaifuExtensions -{ - public static WaifuInfo ByWaifuUserId( - this DbSet waifus, - ulong userId, - Func, IQueryable> includes = null) - { - if (includes is null) - { - return waifus.Include(wi => wi.Waifu) - .Include(wi => wi.Affinity) - .Include(wi => wi.Claimer) - .Include(wi => wi.Items) - .FirstOrDefault(wi => wi.Waifu.UserId == userId); - } - - return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId); - } - - public static IEnumerable GetTop(this DbSet waifus, int count, int skip = 0) - { - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); - if (count == 0) - return new List(); - - return waifus.Include(wi => wi.Waifu) - .Include(wi => wi.Affinity) - .Include(wi => wi.Claimer) - .OrderByDescending(wi => wi.Price) - .Skip(skip) - .Take(count) - .Select(x => new WaifuLbResult - { - Affinity = x.Affinity == null ? null : x.Affinity.Username, - AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, - Claimer = x.Claimer == null ? null : x.Claimer.Username, - ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, - Username = x.Waifu.Username, - Discrim = x.Waifu.Discriminator, - Price = x.Price - }) - .ToList(); - } - - public static decimal GetTotalValue(this DbSet waifus) - => waifus.AsQueryable().Where(x => x.ClaimerId != null).Sum(x => x.Price); - - public static ulong GetWaifuUserId(this DbSet waifus, ulong ownerId, string name) - => waifus.AsQueryable() - .AsNoTracking() - .Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name) - .Select(x => x.Waifu.UserId) - .FirstOrDefault(); - - public static async Task GetWaifuInfoAsync(this DbContext ctx, ulong userId) - { - await ctx.Set() - .ToLinqToDBTable() - .InsertOrUpdateAsync(() => new() - { - AffinityId = null, - ClaimerId = null, - Price = 1, - WaifuId = ctx.Set().Where(x => x.UserId == userId).Select(x => x.Id).First() - }, - _ => new(), - () => new() - { - WaifuId = ctx.Set().Where(x => x.UserId == userId).Select(x => x.Id).First() - }); - - var toReturn = ctx.Set().AsQueryable() - .Where(w => w.WaifuId - == ctx.Set() - .AsQueryable() - .Where(u => u.UserId == userId) - .Select(u => u.Id) - .FirstOrDefault()) - .Select(w => new WaifuInfoStats - { - WaifuId = w.WaifuId, - FullName = - ctx.Set() - .AsQueryable() - .Where(u => u.UserId == userId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), - AffinityCount = - ctx.Set() - .AsQueryable() - .Count(x => x.UserId == w.WaifuId - && x.UpdateType == WaifuUpdateType.AffinityChanged - && x.NewId != null), - AffinityName = - ctx.Set() - .AsQueryable() - .Where(u => u.Id == w.AffinityId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), - ClaimCount = ctx.Set().AsQueryable().Count(x => x.ClaimerId == w.WaifuId), - ClaimerName = - ctx.Set() - .AsQueryable() - .Where(u => u.Id == w.ClaimerId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), - DivorceCount = - ctx.Set() - .AsQueryable() - .Count(x => x.OldId == w.WaifuId - && x.NewId == null - && x.UpdateType == WaifuUpdateType.Claimed), - Price = w.Price, - }) - .FirstOrDefault(); - - if (toReturn is null) - return null; - - return toReturn; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuInfoStats.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuInfoStats.cs deleted file mode 100644 index 944488a..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuInfoStats.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -namespace Ellie.Db; - -public class WaifuInfoStats -{ - public int WaifuId { get; init; } - public string FullName { get; init; } - public long Price { get; init; } - public string ClaimerName { get; init; } - public string AffinityName { get; init; } - public int AffinityCount { get; init; } - public int DivorceCount { get; init; } - public int ClaimCount { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuItem.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuItem.cs deleted file mode 100644 index 95491c8..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuItem.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class WaifuItem : DbEntity -{ - public WaifuInfo WaifuInfo { get; set; } - public int? WaifuInfoId { get; set; } - public string ItemEmoji { get; set; } - public string Name { get; set; } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuLbResult.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuLbResult.cs deleted file mode 100644 index 4eabfba..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuLbResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class WaifuLbResult -{ - public string Username { get; set; } - public string Discrim { get; set; } - - public string Claimer { get; set; } - public string ClaimerDiscrim { get; set; } - - public string Affinity { get; set; } - public string AffinityDiscrim { get; set; } - - public long Price { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdate.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdate.cs deleted file mode 100644 index b2b7871..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdate.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -using Ellie.Db.Models; - -namespace Ellie.Services.Database.Models; - -public class WaifuUpdate : DbEntity -{ - public int UserId { get; set; } - public DiscordUser User { get; set; } - public WaifuUpdateType UpdateType { get; set; } - - public int? OldId { get; set; } - public DiscordUser Old { get; set; } - - public int? NewId { get; set; } - public DiscordUser New { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdateType.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdateType.cs deleted file mode 100644 index 68323f4..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/Waifus/db/WaifuUpdateType.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public enum WaifuUpdateType -{ - AffinityChanged, - Claimed -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/Decks/QuadDeck.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/Decks/QuadDeck.cs deleted file mode 100644 index f1adb06..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/Decks/QuadDeck.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Ellie.Econ; - -namespace Ellie.Modules.Gambling.Common; - -public class QuadDeck : Deck -{ - protected override void RefillPool() - { - CardPool = new(52 * 4); - for (var j = 1; j < 14; j++) - for (var i = 1; i < 5; i++) - { - CardPool.Add(new((CardSuit)i, j)); - CardPool.Add(new((CardSuit)i, j)); - CardPool.Add(new((CardSuit)i, j)); - CardPool.Add(new((CardSuit)i, j)); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingCleanupService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingCleanupService.cs deleted file mode 100644 index d465f1f..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingCleanupService.cs +++ /dev/null @@ -1,68 +0,0 @@ -using LinqToDB; -using Ellie.Db.Models; -using Ellie.Services.Database.Models; - -namespace Ellie.Bot.Modules.Gambling.Gambling._Common; - -// todo organize -public interface IGamblingCleanupService -{ - Task DeleteWaifus(); - Task DeleteWaifu(ulong userId); - Task DeleteCurrency(); -} - -public class GamblingCleanupService : IGamblingCleanupService, IEService -{ - private readonly DbService _db; - - public GamblingCleanupService(DbService db) - { - _db = db; - } - - public async Task DeleteWaifus() - { - await using var ctx = _db.GetDbContext(); - await ctx.Set().DeleteAsync(); - await ctx.Set().DeleteAsync(); - await ctx.Set().DeleteAsync(); - await ctx.SaveChangesAsync(); - } - - public async Task DeleteWaifu(ulong userId) - { - await using var ctx = _db.GetDbContext(); - await ctx.Set() - .Where(x => x.User.UserId == userId) - .DeleteAsync(); - await ctx.Set() - .Where(x => x.WaifuInfo.Waifu.UserId == userId) - .DeleteAsync(); - await ctx.Set() - .Where(x => x.Claimer.UserId == userId) - .UpdateAsync(old => new WaifuInfo() - { - ClaimerId = null, - }); - await ctx.Set() - .Where(x => x.Waifu.UserId == userId) - .DeleteAsync(); - await ctx.SaveChangesAsync(); - } - - public async Task DeleteCurrency() - { - await using var uow = _db.GetDbContext(); - await uow.Set().UpdateAsync(_ => new DiscordUser() - { - CurrencyAmount = 0 - }); - - await uow.Set().DeleteAsync(); - await uow.Set().DeleteAsync(); - await uow.Set().DeleteAsync(); - await uow.SaveChangesAsync(); - } - -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingService.cs deleted file mode 100644 index 9e67852..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/IGamblingService.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable disable -using Ellie.Econ.Gambling; -using Ellie.Econ.Gambling.Betdraw; -using Ellie.Econ.Gambling.Rps; -using OneOf; - -namespace Ellie.Modules.Gambling; - -public interface IGamblingService -{ - Task> LulaAsync(ulong userId, long amount); - Task> BetRollAsync(ulong userId, long amount); - Task> BetFlipAsync(ulong userId, long amount, byte guess); - Task> SlotAsync(ulong userId, long amount); - Task FlipAsync(int count); - Task> RpsAsync(ulong userId, long amount, byte pick); - Task> BetDrawAsync(ulong userId, long amount, byte? guessValue, byte? guessColor); -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/NewGamblingService.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/NewGamblingService.cs deleted file mode 100644 index 150e0a2..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/NewGamblingService.cs +++ /dev/null @@ -1,279 +0,0 @@ -#nullable disable -using Ellie.Econ.Gambling; -using Ellie.Econ.Gambling.Betdraw; -using Ellie.Econ.Gambling.Rps; -using Ellie.Modules.Gambling.Services; -using OneOf; - -namespace Ellie.Modules.Gambling; - -public sealed class NewGamblingService : IGamblingService, IEService -{ - private readonly GamblingConfigService _bcs; - private readonly ICurrencyService _cs; - - public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs) - { - _bcs = bcs; - _cs = cs; - } - - public async Task> LulaAsync(ulong userId, long amount) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("lula", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var game = new LulaGame(_bcs.Data.LuckyLadder.Multipliers); - var result = game.Spin(amount); - - var won = (long)result.Won; - if (won > 0) - { - await _cs.AddAsync(userId, won, new("lula", "win")); - } - - return result; - } - - public async Task> BetRollAsync(ulong userId, long amount) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betroll", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var game = new BetrollGame(_bcs.Data.BetRoll.Pairs - .Select(x => (x.WhenAbove, (decimal)x.MultiplyBy)) - .ToList()); - - var result = game.Roll(amount); - - var won = (long)result.Won; - if (won > 0) - { - await _cs.AddAsync(userId, won, new("betroll", "win")); - } - - return result; - } - - public async Task> BetFlipAsync(ulong userId, long amount, byte guess) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (guess > 1) - throw new ArgumentOutOfRangeException(nameof(guess)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betflip", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var game = new BetflipGame(_bcs.Data.BetFlip.Multiplier); - var result = game.Flip(guess, amount); - - var won = (long)result.Won; - if (won > 0) - { - await _cs.AddAsync(userId, won, new("betflip", "win")); - } - - return result; - } - - public async Task> BetDrawAsync(ulong userId, long amount, byte? guessValue, byte? guessColor) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (guessColor is null && guessValue is null) - throw new ArgumentNullException(); - - if (guessColor > 1) - throw new ArgumentOutOfRangeException(nameof(guessColor)); - - if (guessValue > 1) - throw new ArgumentOutOfRangeException(nameof(guessValue)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betdraw", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var game = new BetdrawGame(); - var result = game.Draw((BetdrawValueGuess?)guessValue, (BetdrawColorGuess?)guessColor, amount); - - var won = (long)result.Won; - if (won > 0) - { - await _cs.AddAsync(userId, won, new("betdraw", "win")); - } - - return result; - } - - public async Task> SlotAsync(ulong userId, long amount) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("slot", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var game = new SlotGame(); - var result = game.Spin(amount); - - var won = (long)result.Won; - if (won > 0) - { - await _cs.AddAsync(userId, won, new("slot", "won")); - } - - return result; - } - - public Task FlipAsync(int count) - { - if (count < 1) - throw new ArgumentOutOfRangeException(nameof(count)); - - var game = new BetflipGame(0); - - var results = new FlipResult[count]; - for (var i = 0; i < count; i++) - { - results[i] = new() - { - Side = game.Flip(0, 0).Side - }; - } - - return Task.FromResult(results); - } - - // - // - // private readonly ConcurrentDictionary _decks = new ConcurrentDictionary(); - // - // public override Task DeckShuffle(DeckShuffleRequest request, ServerCallContext context) - // { - // _decks.AddOrUpdate(request.Id, new Deck(), (key, old) => new Deck()); - // return Task.FromResult(new DeckShuffleReply { }); - // } - // - // public override Task DeckDraw(DeckDrawRequest request, ServerCallContext context) - // { - // if (request.Count < 1 || request.Count > 10) - // throw new ArgumentOutOfRangeException(nameof(request.Id)); - // - // var deck = request.UseNew - // ? new Deck() - // : _decks.GetOrAdd(request.Id, new Deck()); - // - // var list = new List(request.Count); - // for (int i = 0; i < request.Count; i++) - // { - // var card = deck.DrawNoRestart(); - // if (card is null) - // { - // if (i == 0) - // { - // deck.Restart(); - // list.Add(deck.DrawNoRestart()); - // continue; - // } - // - // break; - // } - // - // list.Add(card); - // } - // - // var cards = list - // .Select(x => new Card - // { - // Name = x.ToString().ToLowerInvariant().Replace(' ', '_'), - // Number = x.Number, - // Suit = (CardSuit) x.Suit - // }); - // - // var toReturn = new DeckDrawReply(); - // toReturn.Cards.AddRange(cards); - // - // return Task.FromResult(toReturn); - // } - // - - public async Task> RpsAsync(ulong userId, long amount, byte pick) - { - if (amount < 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (pick > 2) - throw new ArgumentOutOfRangeException(nameof(pick)); - - if (amount > 0) - { - var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("rps", "bet")); - - if (!isTakeSuccess) - { - return GamblingError.InsufficientFunds; - } - } - - var rps = new RpsGame(); - var result = rps.Play((RpsPick)pick, amount); - - var won = (long)result.Won; - if (won > 0) - { - var extra = result.Result switch - { - RpsResultType.Draw => "draw", - RpsResultType.Win => "win", - _ => "lose" - }; - - await _cs.AddAsync(userId, won, new("rps", extra)); - } - - return result; - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/RollDuelGame.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/RollDuelGame.cs deleted file mode 100644 index 4add729..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/RollDuelGame.cs +++ /dev/null @@ -1,139 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Gambling.Common; - -public class RollDuelGame -{ - public enum Reason - { - Normal, - NoFunds, - Timeout - } - - public enum State - { - Waiting, - Running, - Ended - } - - public event Func OnGameTick; - public event Func OnEnded; - - public ulong P1 { get; } - public ulong P2 { get; } - - public long Amount { get; } - - public List<(int, int)> Rolls { get; } = new(); - public State CurrentState { get; private set; } - public ulong Winner { get; private set; } - - private readonly ulong _botId; - - private readonly ICurrencyService _cs; - - private readonly Timer _timeoutTimer; - private readonly EllieRandom _rng = new(); - private readonly SemaphoreSlim _locker = new(1, 1); - - public RollDuelGame( - ICurrencyService cs, - ulong botId, - ulong p1, - ulong p2, - long amount) - { - P1 = p1; - P2 = p2; - _botId = botId; - Amount = amount; - _cs = cs; - - _timeoutTimer = new(async delegate - { - await _locker.WaitAsync(); - try - { - if (CurrentState != State.Waiting) - return; - CurrentState = State.Ended; - await OnEnded?.Invoke(this, Reason.Timeout); - } - catch { } - finally - { - _locker.Release(); - } - }, - null, - TimeSpan.FromSeconds(15), - TimeSpan.FromMilliseconds(-1)); - } - - public async Task StartGame() - { - await _locker.WaitAsync(); - try - { - if (CurrentState != State.Waiting) - return; - _timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - CurrentState = State.Running; - } - finally - { - _locker.Release(); - } - - if (!await _cs.RemoveAsync(P1, Amount, new("rollduel", "bet"))) - { - await OnEnded?.Invoke(this, Reason.NoFunds); - CurrentState = State.Ended; - return; - } - - if (!await _cs.RemoveAsync(P2, Amount, new("rollduel", "bet"))) - { - await _cs.AddAsync(P1, Amount, new("rollduel", "refund")); - await OnEnded?.Invoke(this, Reason.NoFunds); - CurrentState = State.Ended; - return; - } - - int n1, n2; - do - { - n1 = _rng.Next(0, 5); - n2 = _rng.Next(0, 5); - Rolls.Add((n1, n2)); - if (n1 != n2) - { - if (n1 > n2) - Winner = P1; - else - Winner = P2; - var won = (long)(Amount * 2 * 0.98f); - await _cs.AddAsync(Winner, won, new("rollduel", "win")); - - await _cs.AddAsync(_botId, (Amount * 2) - won, new("rollduel", "fee")); - } - - try { await OnGameTick?.Invoke(this); } - catch { } - - await Task.Delay(2500); - if (n1 != n2) - break; - } while (true); - - CurrentState = State.Ended; - await OnEnded?.Invoke(this, Reason.Normal); - } -} - -public struct RollDuelChallenge -{ - public ulong Player1 { get; set; } - public ulong Player2 { get; set; } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/BaseShmartInputAmountReader.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/BaseShmartInputAmountReader.cs deleted file mode 100644 index 47abcce..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/BaseShmartInputAmountReader.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Text.RegularExpressions; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Gambling.Services; -using NCalc; -using OneOf; - -namespace Ellie.Common.TypeReaders; - -public class BaseShmartInputAmountReader -{ - private static readonly Regex _percentRegex = new(@"^((?100|\d{1,2})%)$", RegexOptions.Compiled); - protected readonly DbService _db; - protected readonly GamblingConfigService _gambling; - - public BaseShmartInputAmountReader(DbService db, GamblingConfigService gambling) - { - _db = db; - _gambling = gambling; - } - - public async ValueTask>> ReadAsync(ICommandContext context, string input) - { - var i = input.Trim().ToUpperInvariant(); - - i = i.Replace("K", "000"); - - //can't add m because it will conflict with max atm - - if (await TryHandlePercentage(context, i) is long num) - { - return num; - } - - try - { - var expr = new Expression(i, EvaluateOptions.IgnoreCase); - expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context).GetAwaiter().GetResult(); - return (long)decimal.Parse(expr.Evaluate().ToString()!); - } - catch (Exception) - { - return new OneOf.Types.Error($"Invalid input: {input}"); - } - } - - private async Task EvaluateParam(string name, ParameterArgs args, ICommandContext ctx) - { - switch (name.ToUpperInvariant()) - { - case "PI": - args.Result = Math.PI; - break; - case "E": - args.Result = Math.E; - break; - case "ALL": - case "ALLIN": - args.Result = await Cur(ctx); - break; - case "HALF": - args.Result = await Cur(ctx) / 2; - break; - case "MAX": - args.Result = await Max(ctx); - break; - } - } - - protected virtual async Task Cur(ICommandContext ctx) - { - await using var uow = _db.GetDbContext(); - return await uow.Set().GetUserCurrencyAsync(ctx.User.Id); - } - - protected virtual async Task Max(ICommandContext ctx) - { - var settings = _gambling.Data; - var max = settings.MaxBet; - return max == 0 ? await Cur(ctx) : max; - } - - private async Task TryHandlePercentage(ICommandContext ctx, string input) - { - var m = _percentRegex.Match(input); - - if (m.Captures.Count == 0) - return null; - - if (!long.TryParse(m.Groups["num"].ToString(), out var percent)) - return null; - - return (long)(await Cur(ctx) * (percent / 100.0f)); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartBankInputAmountReader.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartBankInputAmountReader.cs deleted file mode 100644 index 49ad387..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartBankInputAmountReader.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ellie.Modules.Gambling.Bank; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Common.TypeReaders; - -public sealed class ShmartBankInputAmountReader : BaseShmartInputAmountReader -{ - private readonly IBankService _bank; - - public ShmartBankInputAmountReader(IBankService bank, DbService db, GamblingConfigService gambling) - : base(db, gambling) - { - _bank = bank; - } - - protected override Task Cur(ICommandContext ctx) - => _bank.GetBalanceAsync(ctx.User.Id); - - protected override Task Max(ICommandContext ctx) - => Cur(ctx); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartNumberTypeReader.cs b/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartNumberTypeReader.cs deleted file mode 100644 index e39916f..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Gambling/_common/TypeReaders/ShmartNumberTypeReader.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -using Ellie.Modules.Gambling.Bank; -using Ellie.Modules.Gambling.Services; - -namespace Ellie.Common.TypeReaders; - -public sealed class BalanceTypeReader : TypeReader -{ - private readonly BaseShmartInputAmountReader _tr; - - public BalanceTypeReader(DbService db, GamblingConfigService gambling) - { - _tr = new BaseShmartInputAmountReader(db, gambling); - } - - public override async Task ReadAsync( - ICommandContext context, - string input, - IServiceProvider services) - { - - var result = await _tr.ReadAsync(context, input); - - if (result.TryPickT0(out var val, out var err)) - { - return Discord.Commands.TypeReaderResult.FromSuccess(val); - } - - return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value); - } -} - -public sealed class BankBalanceTypeReader : TypeReader -{ - private readonly ShmartBankInputAmountReader _tr; - - public BankBalanceTypeReader(IBankService bank, DbService db, GamblingConfigService gambling) - { - _tr = new ShmartBankInputAmountReader(bank, db, gambling); - } - - public override async Task ReadAsync( - ICommandContext context, - string input, - IServiceProvider services) - { - - var result = await _tr.ReadAsync(context, input); - - if (result.TryPickT0(out var val, out var err)) - { - return Discord.Commands.TypeReaderResult.FromSuccess(val); - } - - return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value); - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/Acrophobia.cs b/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/Acrophobia.cs deleted file mode 100644 index 66b6bdd..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/Acrophobia.cs +++ /dev/null @@ -1,200 +0,0 @@ -#nullable disable -using CommandLine; -using System.Collections.Immutable; - -namespace Ellie.Modules.Games.Common.Acrophobia; - -public sealed class AcrophobiaGame : IDisposable -{ - public enum Phase - { - Submission, - Voting, - Ended - } - - public enum UserInputResult - { - Submitted, - SubmissionFailed, - Voted, - VotingFailed, - Failed - } - - public event Func OnStarted = delegate { return Task.CompletedTask; }; - - public event Func>, Task> OnVotingStarted = - delegate { return Task.CompletedTask; }; - - public event Func OnUserVoted = delegate { return Task.CompletedTask; }; - - public event Func>, Task> OnEnded = delegate - { - return Task.CompletedTask; - }; - - public Phase CurrentPhase { get; private set; } = Phase.Submission; - public ImmutableArray StartingLetters { get; private set; } - public Options Opts { get; } - - private readonly Dictionary _submissions = new(); - private readonly SemaphoreSlim _locker = new(1, 1); - private readonly EllieRandom _rng; - - private readonly HashSet _usersWhoVoted = new(); - - public AcrophobiaGame(Options options) - { - Opts = options; - _rng = new(); - InitializeStartingLetters(); - } - - public async Task Run() - { - await OnStarted(this); - await Task.Delay(Opts.SubmissionTime * 1000); - await _locker.WaitAsync(); - try - { - if (_submissions.Count == 0) - { - CurrentPhase = Phase.Ended; - await OnVotingStarted(this, ImmutableArray.Create>()); - return; - } - - if (_submissions.Count == 1) - { - CurrentPhase = Phase.Ended; - await OnVotingStarted(this, _submissions.ToArray().ToImmutableArray()); - return; - } - - CurrentPhase = Phase.Voting; - - await OnVotingStarted(this, _submissions.ToArray().ToImmutableArray()); - } - finally { _locker.Release(); } - - await Task.Delay(Opts.VoteTime * 1000); - await _locker.WaitAsync(); - try - { - CurrentPhase = Phase.Ended; - await OnEnded(this, _submissions.ToArray().ToImmutableArray()); - } - finally { _locker.Release(); } - } - - private void InitializeStartingLetters() - { - var wordCount = _rng.Next(3, 6); - - var lettersArr = new char[wordCount]; - - for (var i = 0; i < wordCount; i++) - { - var randChar = (char)_rng.Next(65, 91); - lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; - } - - StartingLetters = lettersArr.ToImmutableArray(); - } - - public async Task UserInput(ulong userId, string userName, string input) - { - var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase()); - - await _locker.WaitAsync(); - try - { - switch (CurrentPhase) - { - case Phase.Submission: - if (_submissions.ContainsKey(user) || !IsValidAnswer(input)) - break; - - _submissions.Add(user, 0); - return true; - case Phase.Voting: - AcrophobiaUser toVoteFor; - if (!int.TryParse(input, out var index) - || --index < 0 - || index >= _submissions.Count - || (toVoteFor = _submissions.ToArray()[index].Key).UserId == user.UserId - || !_usersWhoVoted.Add(userId)) - break; - ++_submissions[toVoteFor]; - _ = Task.Run(() => OnUserVoted(userName)); - return true; - } - - return false; - } - finally - { - _locker.Release(); - } - } - - private bool IsValidAnswer(string input) - { - input = input.ToUpperInvariant(); - - var inputWords = input.Split(' '); - - if (inputWords.Length - != StartingLetters.Length) // number of words must be the same as the number of the starting letters - return false; - - for (var i = 0; i < StartingLetters.Length; i++) - { - var letter = StartingLetters[i]; - - if (!inputWords[i] - .StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match - return false; - } - - return true; - } - - public void Dispose() - { - CurrentPhase = Phase.Ended; - OnStarted = null; - OnEnded = null; - OnUserVoted = null; - OnVotingStarted = null; - _usersWhoVoted.Clear(); - _submissions.Clear(); - _locker.Dispose(); - } - - public class Options : IEllieCommandOptions - { - [Option('s', - "submission-time", - Required = false, - Default = 60, - HelpText = "Time after which the submissions are closed and voting starts.")] - public int SubmissionTime { get; set; } = 60; - - [Option('v', - "vote-time", - Required = false, - Default = 60, - HelpText = "Time after which the voting is closed and the winner is declared.")] - public int VoteTime { get; set; } = 30; - - public void NormalizeOptions() - { - if (SubmissionTime is < 15 or > 300) - SubmissionTime = 60; - if (VoteTime is < 15 or > 120) - VoteTime = 30; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcrophobiaUser.cs b/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcrophobiaUser.cs deleted file mode 100644 index 83ba8e7..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcrophobiaUser.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Common.Acrophobia; - -public class AcrophobiaUser -{ - public string UserName { get; } - public ulong UserId { get; } - public string Input { get; } - - public AcrophobiaUser(ulong userId, string userName, string input) - { - UserName = userName; - UserId = userId; - Input = input; - } - - public override int GetHashCode() - => UserId.GetHashCode(); - - public override bool Equals(object obj) - => obj is AcrophobiaUser x ? x.UserId == UserId : false; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcropobiaCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcropobiaCommands.cs deleted file mode 100644 index 6dfb0d3..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Acrophobia/AcropobiaCommands.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common.Acrophobia; -using Ellie.Modules.Games.Services; -using System.Collections.Immutable; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class AcropobiaCommands : EllieModule - { - private readonly DiscordSocketClient _client; - - public AcropobiaCommands(DiscordSocketClient client) - => _client = client; - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public async Task Acrophobia(params string[] args) - { - var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args); - var channel = (ITextChannel)ctx.Channel; - - var game = new AcrophobiaGame(options); - if (_service.AcrophobiaGames.TryAdd(channel.Id, game)) - { - try - { - game.OnStarted += Game_OnStarted; - game.OnEnded += Game_OnEnded; - game.OnVotingStarted += Game_OnVotingStarted; - game.OnUserVoted += Game_OnUserVoted; - _client.MessageReceived += ClientMessageReceived; - await game.Run(); - } - finally - { - _client.MessageReceived -= ClientMessageReceived; - _service.AcrophobiaGames.TryRemove(channel.Id, out game); - game?.Dispose(); - } - } - else - await ReplyErrorLocalizedAsync(strs.acro_running); - - Task ClientMessageReceived(SocketMessage msg) - { - if (msg.Channel.Id != ctx.Channel.Id) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - try - { - var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content); - if (success) - await msg.DeleteAsync(); - } - catch { } - }); - - return Task.CompletedTask; - } - } - - private Task Game_OnStarted(AcrophobiaGame game) - { - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.acrophobia)) - .WithDescription( - GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters))))) - .WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime))); - - return ctx.Channel.EmbedAsync(embed); - } - - private Task Game_OnUserVoted(string user) - => SendConfirmAsync(GetText(strs.acrophobia), GetText(strs.acro_vote_cast(Format.Bold(user)))); - - private async Task Game_OnVotingStarted( - AcrophobiaGame game, - ImmutableArray> submissions) - { - if (submissions.Length == 0) - { - await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub)); - return; - } - - if (submissions.Length == 1) - { - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithDescription(GetText( - strs.acro_winner_only( - Format.Bold(submissions.First().Key.UserName)))) - .WithFooter(submissions.First().Key.Input)); - return; - } - - - var i = 0; - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed)) - .WithDescription(GetText(strs.acro_nym_was( - Format.Bold(string.Join(".", game.StartingLetters)) - + "\n" - + $@"-- -{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")} ---"))) - .WithFooter(GetText(strs.acro_vote)); - - await ctx.Channel.EmbedAsync(embed); - } - - private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray> votes) - { - if (!votes.Any() || votes.All(x => x.Value == 0)) - { - await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast)); - return; - } - - var table = votes.OrderByDescending(v => v.Value); - var winner = table.First(); - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.acrophobia)) - .WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName), - Format.Bold(winner.Value.ToString())))) - .WithFooter(winner.Key.Input); - - await ctx.Channel.EmbedAsync(embed); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs deleted file mode 100644 index 06f1e7d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs +++ /dev/null @@ -1,217 +0,0 @@ -#nullable disable -using Ellie.Bot.Common; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db.Models; -using Ellie.Modules.Games.Common; -using Ellie.Modules.Games.Common.ChatterBot; -using Ellie.Modules.Patronage; -using Ellie.Modules.Permissions; - -namespace Ellie.Modules.Games.Services; - -public class ChatterBotService : IExecOnMessage -{ - public ConcurrentDictionary> ChatterBotGuilds { get; } - - public int Priority - => 1; - - private readonly FeatureLimitKey _flKey; - - private readonly DiscordSocketClient _client; - private readonly IPermissionChecker _perms; - private readonly CommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly IBotCredentials _creds; - private readonly IEmbedBuilderService _eb; - private readonly IHttpClientFactory _httpFactory; - private readonly IPatronageService _ps; - private readonly GamesConfigService _gcs; - - public ChatterBotService( - DiscordSocketClient client, - IPermissionChecker perms, - IBot bot, - CommandHandler cmd, - IBotStrings strings, - IHttpClientFactory factory, - IBotCredentials creds, - IEmbedBuilderService eb, - IPatronageService ps, - GamesConfigService gcs) - { - _client = client; - _perms = perms; - _cmd = cmd; - _strings = strings; - _creds = creds; - _eb = eb; - _httpFactory = factory; - _ps = ps; - _perms = perms; - _gcs = gcs; - - _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))); - } - - public IChatterBotSession CreateSession() - { - switch (_gcs.Data.ChatBot) - { - case ChatBotImplementation.Cleverbot: - if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) - return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory); - - Log.Information("Cleverbot will not work as the api key is missing."); - return null; - case ChatBotImplementation.Gpt3: - if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) - return new OfficialGpt3Session(_creds.Gpt3ApiKey, - _gcs.Data.ChatGpt.Model, - _gcs.Data.ChatGpt.MaxTokens, - _httpFactory); - - Log.Information("Gpt3 will not work as the api key is missing."); - return null; - default: - return null; - } - } - - public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) - { - var channel = msg.Channel as ITextChannel; - cleverbot = null; - - if (channel is null) - return null; - - if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot)) - return null; - - cleverbot = lazyCleverbot.Value; - - var nadekoId = _client.CurrentUser.Id; - var normalMention = $"<@{nadekoId}> "; - var nickMention = $"<@!{nadekoId}> "; - string message; - if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) - message = msg.Content[normalMention.Length..].Trim(); - else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture)) - message = msg.Content[nickMention.Length..].Trim(); - else - return null; - - return message; - } - - public async Task ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg) - { - if (guild is not SocketGuild sg) - return false; - - try - { - var message = PrepareMessage(usrMsg, out var cbs); - if (message is null || cbs is null) - return false; - - var res = await _perms.CheckAsync(sg, - usrMsg.Channel, - usrMsg.Author, - "games", - CleverBotResponseStr.CLEVERBOT_RESPONSE); - - // todo this needs checking, this might block all messages in a channel if cleverbot is enabled but blocked - // need to check what kind of block it is - // might be the case for other classes using permission checker - if (!res.IsT0) - return true; - - var channel = (ITextChannel)usrMsg.Channel; - var conf = _ps.GetConfig(); - if (!_creds.IsOwner(sg.OwnerId) && 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/emotionchild) 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); - - return true; - } - catch (Exception ex) - { - Log.Warning(ex, "Error in cleverbot"); - } - - return false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/CleverBotCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/CleverBotCommands.cs deleted file mode 100644 index c1895f4..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/CleverBotCommands.cs +++ /dev/null @@ -1,48 +0,0 @@ -#nullable disable -using Ellie.Db; -using Ellie.Modules.Games.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class ChatterBotCommands : EllieModule - { - private readonly DbService _db; - - public ChatterBotCommands(DbService db) - => _db = db; - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task CleverBot() - { - var channel = (ITextChannel)ctx.Channel; - - if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) - { - await using (var uow = _db.GetDbContext()) - { - uow.Set().SetCleverbotEnabled(ctx.Guild.Id, false); - await uow.SaveChangesAsync(); - } - - await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled); - return; - } - - _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new(() => _service.CreateSession(), true)); - - await using (var uow = _db.GetDbContext()) - { - uow.Set().SetCleverbotEnabled(ctx.Guild.Id, true); - await uow.SaveChangesAsync(); - } - - await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/CleverbotResponse.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/CleverbotResponse.cs deleted file mode 100644 index 6dc4db1..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/CleverbotResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Common.ChatterBot; - -public class CleverbotResponse -{ - public string Cs { get; set; } - public string Output { get; set; } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/Gpt3Response.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/Gpt3Response.cs deleted file mode 100644 index 6051c94..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/Gpt3Response.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Games.Common.ChatterBot; - -public class Gpt3Response -{ - [JsonPropertyName("choices")] - public Choice[] Choices { get; set; } -} - -public class Choice -{ - public string Text { get; set; } -} - -public class Gpt3ApiRequest -{ - [JsonPropertyName("model")] - public string Model { get; init; } - - [JsonPropertyName("prompt")] - public string Prompt { get; init; } - - [JsonPropertyName("temperature")] - public int Temperature { get; init; } - - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; init; } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/IChatterBotSession.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/IChatterBotSession.cs deleted file mode 100644 index c16cec3..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/IChatterBotSession.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Common.ChatterBot; - -public interface IChatterBotSession -{ - Task Think(string input); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialCleverbotSession.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialCleverbotSession.cs deleted file mode 100644 index f967b1b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialCleverbotSession.cs +++ /dev/null @@ -1,38 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Modules.Games.Common.ChatterBot; - -public class OfficialCleverbotSession : IChatterBotSession -{ - private string QueryString - => $"https://www.cleverbot.com/getreply?key={_apiKey}" + "&wrapper=ellie" + "&input={0}" + "&cs={1}"; - - private readonly string _apiKey; - private readonly IHttpClientFactory _httpFactory; - private string cs; - - public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory) - { - _apiKey = apiKey; - _httpFactory = factory; - } - - public async Task Think(string input) - { - using var http = _httpFactory.CreateClient(); - var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? "")); - try - { - var data = JsonConvert.DeserializeObject(dataString); - - cs = data?.Cs; - return data?.Output; - } - catch - { - Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString); - return null; - } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialGpt3Session.cs b/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialGpt3Session.cs deleted file mode 100644 index 4caaeb9..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/ChatterBot/_common/OfficialGpt3Session.cs +++ /dev/null @@ -1,68 +0,0 @@ -#nullable disable -using Newtonsoft.Json; -using System.Net.Http.Json; - -namespace Ellie.Modules.Games.Common.ChatterBot; - -public class OfficialGpt3Session : IChatterBotSession -{ - private string Uri - => $"https://api.openai.com/v1/completions"; - - private readonly string _apiKey; - private readonly string _model; - private readonly int _maxTokens; - private readonly IHttpClientFactory _httpFactory; - - public OfficialGpt3Session( - string apiKey, - Gpt3Model model, - int maxTokens, - IHttpClientFactory factory) - { - _apiKey = apiKey; - _httpFactory = factory; - switch (model) - { - case Gpt3Model.Ada001: - _model = "text-ada-001"; - break; - case Gpt3Model.Babbage001: - _model = "text-babbage-001"; - break; - case Gpt3Model.Curie001: - _model = "text-curie-001"; - break; - case Gpt3Model.Davinci003: - _model = "text-davinci-003"; - break; - } - - _maxTokens = maxTokens; - } - - public async Task Think(string input) - { - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey); - var data = await http.PostAsJsonAsync(Uri, new Gpt3ApiRequest() - { - Model = _model, - Prompt = input, - MaxTokens = _maxTokens, - Temperature = 1, - }); - var dataString = await data.Content.ReadAsStringAsync(); - try - { - var response = JsonConvert.DeserializeObject(dataString); - - return response?.Choices[0]?.Text; - } - catch - { - Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString); - return null; - } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Games.cs b/src/Ellie.Bot.Modules.Gambling/Games/Games.cs deleted file mode 100644 index c97acdc..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Games.cs +++ /dev/null @@ -1,150 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Games; - -/* more games -- Shiritori -- Simple RPG adventure -*/ -public partial class Games : EllieModule -{ - private readonly IImageCache _images; - private readonly IHttpClientFactory _httpFactory; - private readonly Random _rng = new(); - - public Games(IImageCache images, IHttpClientFactory factory) - { - _images = images; - _httpFactory = factory; - } - - [Cmd] - public async Task Choose([Leftover] string list = null) - { - if (string.IsNullOrWhiteSpace(list)) - return; - var listArr = list.Split(';'); - if (listArr.Length < 2) - return; - var rng = new EllieRandom(); - await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]); - } - - [Cmd] - public async Task EightBall([Leftover] string question = null) - { - if (string.IsNullOrWhiteSpace(question)) - return; - - var res = _service.GetEightballResponse(ctx.User.Id, question); - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithDescription(ctx.User.ToString()) - .AddField("❓ " + GetText(strs.question), question) - .AddField("🎱 " + GetText(strs._8ball), res)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RateGirl([Leftover] IGuildUser usr) - { - var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl); - var originalStream = await gr.Stream; - - if (originalStream is null) - { - await ReplyErrorLocalizedAsync(strs.something_went_wrong); - return; - } - - await using var imgStream = new MemoryStream(); - lock (gr) - { - originalStream.Position = 0; - originalStream.CopyTo(imgStream); - } - - imgStream.Position = 0; - await ctx.Channel.SendFileAsync(imgStream, - $"rating.png", - Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"), - embed: _eb.Create() - .WithOkColor() - .AddField("Hot", gr.Hot.ToString("F2"), true) - .AddField("Crazy", gr.Crazy.ToString("F2"), true) - .AddField("Advice", gr.Advice) - .WithImageUrl($"attachment://rating.png") - .Build()); - } - - private double NextDouble(double x, double y) - => (_rng.NextDouble() * (y - x)) + x; - - private GirlRating GetGirl(ulong uid) - { - var rng = new EllieRandom(); - - var roll = rng.Next(1, 1001); - - var ratings = _service.Ratings.GetAwaiter().GetResult(); - - double hot; - double crazy; - string advice; - if (roll < 500) - { - hot = NextDouble(0, 5); - crazy = NextDouble(4, 10); - advice = ratings.Nog; - } - else if (roll < 750) - { - hot = NextDouble(5, 8); - crazy = NextDouble(4, (.6 * hot) + 4); - advice = ratings.Fun; - } - else if (roll < 900) - { - hot = NextDouble(5, 10); - crazy = NextDouble((.61 * hot) + 4, 10); - advice = ratings.Dan; - } - else if (roll < 951) - { - hot = NextDouble(8, 10); - crazy = NextDouble(7, (.6 * hot) + 4); - advice = ratings.Dat; - } - else if (roll < 990) - { - hot = NextDouble(8, 10); - crazy = NextDouble(5, 7); - advice = ratings.Wif; - } - else if (roll < 999) - { - hot = NextDouble(8, 10); - crazy = NextDouble(2, 3.99d); - advice = ratings.Tra; - } - else - { - hot = NextDouble(8, 10); - crazy = NextDouble(4, 5); - advice = ratings.Uni; - } - - return new(_images, crazy, hot, roll, advice); - } - - [Cmd] - public async Task Linux(string guhnoo, string loonix) - => await SendConfirmAsync( - $@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. - -Many computer users run a modified version of the {guhnoo} system every day, without realizing it. Through a peculiar turn of events, the version of {guhnoo} which is widely used today is often called {loonix}, and many of its users are not aware that it is basically the {guhnoo} system, developed by the {guhnoo} Project. - -There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}."); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/GamesConfig.cs b/src/Ellie.Bot.Modules.Gambling/Games/GamesConfig.cs deleted file mode 100644 index 9667b8b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/GamesConfig.cs +++ /dev/null @@ -1,160 +0,0 @@ -#nullable disable -using Cloneable; -using Ellie.Common.Yml; - -namespace Ellie.Modules.Games.Common; - -[Cloneable] -public sealed partial class GamesConfig : ICloneable -{ - [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 2; - - [Comment("Hangman related settings (.hangman command)")] - public HangmanConfig Hangman { get; set; } = new() - { - CurrencyReward = 0 - }; - - [Comment("Trivia related settings (.t command)")] - public TriviaConfig Trivia { get; set; } = new() - { - CurrencyReward = 0, - MinimumWinReq = 1 - }; - - [Comment("List of responses for the .8ball command. A random one will be selected every time")] - public List EightBallResponses { get; set; } = new() - { - "Most definitely yes.", - "For sure.", - "Totally!", - "Of course!", - "As I see it, yes.", - "My sources say yes.", - "Yes.", - "Most likely.", - "Perhaps...", - "Maybe...", - "Hm, not sure.", - "It is uncertain.", - "Ask me again later.", - "Don't count on it.", - "Probably not.", - "Very doubtful.", - "Most likely no.", - "Nope.", - "No.", - "My sources say no.", - "Don't even think about it.", - "Definitely no.", - "NO - It may cause disease contraction!" - }; - - [Comment("List of animals which will be used for the animal race game (.race)")] - public List RaceAnimals { get; set; } = new() - { - new() - { - Icon = "🐼", - Name = "Panda" - }, - new() - { - Icon = "🐻", - Name = "Bear" - }, - new() - { - Icon = "🐧", - Name = "Pengu" - }, - new() - { - Icon = "🐨", - Name = "Koala" - }, - new() - { - Icon = "🐬", - Name = "Dolphin" - }, - new() - { - Icon = "🐞", - Name = "Ladybird" - }, - new() - { - Icon = "🦀", - Name = "Crab" - }, - new() - { - Icon = "🦄", - Name = "Unicorn" - } - }; - - [Comment(@"Which chatbot API should bot use. -'cleverbot' - bot will use Cleverbot API. -'gpt3' - bot will use GPT-3 API")] - public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3; - - public ChatGptConfig ChatGpt { get; set; } = new(); -} - -[Cloneable] -public sealed partial class ChatGptConfig -{ - [Comment(@"Which GPT-3 Model should bot use. -'ada001' - cheapest and fastest -'babbage001' - 2nd option -'curie001' - 3rd option -'davinci003' - Most expensive, slowest")] - public Gpt3Model Model { get; set; } = Gpt3Model.Ada001; - - [Comment(@"The maximum number of tokens to use per GPT-3 API call")] - public int MaxTokens { get; set; } = 100; -} - -[Cloneable] -public sealed partial class HangmanConfig -{ - [Comment("The amount of currency awarded to the winner of a hangman game")] - public long CurrencyReward { get; set; } -} - -[Cloneable] -public sealed partial class TriviaConfig -{ - [Comment("The amount of currency awarded to the winner of the trivia game.")] - public long CurrencyReward { get; set; } - - [Comment(""" - Users won't be able to start trivia games which have - a smaller win requirement than the one specified by this setting. - """)] - public int MinimumWinReq { get; set; } = 1; -} - -[Cloneable] -public sealed partial class RaceAnimal -{ - public string Icon { get; set; } - public string Name { get; set; } -} - -public enum ChatBotImplementation -{ - Cleverbot, - Gpt3 -} - -public enum Gpt3Model -{ - Ada001, - Babbage001, - Curie001, - Davinci003 -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/GamesConfigService.cs b/src/Ellie.Bot.Modules.Gambling/Games/GamesConfigService.cs deleted file mode 100644 index 18056b7..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/GamesConfigService.cs +++ /dev/null @@ -1,72 +0,0 @@ -#nullable disable -using Ellie.Common.Configs; -using Ellie.Modules.Games.Common; - -namespace Ellie.Modules.Games.Services; - -public sealed class GamesConfigService : ConfigServiceBase -{ - private const string FILE_PATH = "data/games.yml"; - private static readonly TypedKey _changeKey = new("config.games.updated"); - public override string Name { get; } = "games"; - - public GamesConfigService(IConfigSeria serializer, IPubSub pubSub) - : base(FILE_PATH, serializer, pubSub, _changeKey) - { - AddParsedProp("trivia.min_win_req", - gs => gs.Trivia.MinimumWinReq, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); - AddParsedProp("trivia.currency_reward", - gs => gs.Trivia.CurrencyReward, - long.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - AddParsedProp("hangman.currency_reward", - gs => gs.Hangman.CurrencyReward, - long.TryParse, - ConfigPrinters.ToString, - val => val >= 0); - - AddParsedProp("chatbot", - gs => gs.ChatBot, - ConfigParsers.InsensitiveEnum, - ConfigPrinters.ToString); - AddParsedProp("gpt.model", - gs => gs.ChatGpt.Model, - ConfigParsers.InsensitiveEnum, - ConfigPrinters.ToString); - AddParsedProp("gpt.max_tokens", - gs => gs.ChatGpt.MaxTokens, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); - - Migrate(); - } - - private void Migrate() - { - if (data.Version < 1) - { - ModifyConfig(c => - { - c.Version = 1; - c.Hangman = new() - { - CurrencyReward = 0 - }; - }); - } - - if (data.Version < 2) - { - ModifyConfig(c => - { - c.Version = 2; - c.ChatBot = ChatBotImplementation.Cleverbot; - }); - } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/GamesService.cs b/src/Ellie.Bot.Modules.Gambling/Games/GamesService.cs deleted file mode 100644 index ed09034..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/GamesService.cs +++ /dev/null @@ -1,118 +0,0 @@ -#nullable disable -using Microsoft.Extensions.Caching.Memory; -using Ellie.Common.ModuleBehaviors; -using Ellie.Modules.Games.Common; -using Ellie.Modules.Games.Common.Acrophobia; -using Ellie.Modules.Games.Common.Nunchi; -using Newtonsoft.Json; - -namespace Ellie.Modules.Games.Services; - -public class GamesService : IEService, IReadyExecutor -{ - private const string TYPING_ARTICLES_PATH = "data/typing_articles3.json"; - - public ConcurrentDictionary GirlRatings { get; } = new(); - - public IReadOnlyList EightBallResponses - => _gamesConfig.Data.EightBallResponses; - - public List TypingArticles { get; } = new(); - - //channelId, game - public ConcurrentDictionary AcrophobiaGames { get; } = new(); - public Dictionary TicTacToeGames { get; } = new(); - public ConcurrentDictionary RunningContests { get; } = new(); - public ConcurrentDictionary NunchiGames { get; } = new(); - - public AsyncLazy Ratings { get; } - private readonly GamesConfigService _gamesConfig; - - private readonly IHttpClientFactory _httpFactory; - private readonly IMemoryCache _8BallCache; - private readonly Random _rng; - - public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory) - { - _gamesConfig = gamesConfig; - _httpFactory = httpFactory; - _8BallCache = new MemoryCache(new MemoryCacheOptions - { - SizeLimit = 500_000 - }); - - Ratings = new(GetRatingTexts); - _rng = new EllieRandom(); - - try - { - TypingArticles = JsonConvert.DeserializeObject>(File.ReadAllText(TYPING_ARTICLES_PATH)); - } - catch (Exception ex) - { - Log.Warning(ex, "Error while loading typing articles: {ErrorMessage}", ex.Message); - TypingArticles = new(); - } - } - - public async Task OnReadyAsync() - { - // reset rating once a day - using var timer = new PeriodicTimer(TimeSpan.FromDays(1)); - while (await timer.WaitForNextTickAsync()) - GirlRatings.Clear(); - } - - private async Task GetRatingTexts() - { - using var http = _httpFactory.CreateClient(); - var text = await http.GetStringAsync( - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json"); - return JsonConvert.DeserializeObject(text); - } - - public void AddTypingArticle(IUser user, string text) - { - TypingArticles.Add(new() - { - Source = user.ToString(), - Extra = $"Text added on {DateTime.UtcNow} by {user}.", - Text = text.SanitizeMentions(true) - }); - - File.WriteAllText(TYPING_ARTICLES_PATH, JsonConvert.SerializeObject(TypingArticles)); - } - - public string GetEightballResponse(ulong userId, string question) - => _8BallCache.GetOrCreate($"8ball:{userId}:{question}", - e => - { - e.Size = question.Length; - e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12); - return EightBallResponses[_rng.Next(0, EightBallResponses.Count)]; - }); - - public TypingArticle RemoveTypingArticle(int index) - { - var articles = TypingArticles; - if (index < 0 || index >= articles.Count) - return null; - - var removed = articles[index]; - TypingArticles.RemoveAt(index); - - File.WriteAllText(TYPING_ARTICLES_PATH, JsonConvert.SerializeObject(articles)); - return removed; - } - - public class RatingTexts - { - public string Nog { get; set; } - public string Tra { get; set; } - public string Fun { get; set; } - public string Uni { get; set; } - public string Wif { get; set; } - public string Dat { get; set; } - public string Dan { get; set; } - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/GirlRating.cs b/src/Ellie.Bot.Modules.Gambling/Games/GirlRating.cs deleted file mode 100644 index a391e4b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/GirlRating.cs +++ /dev/null @@ -1,61 +0,0 @@ -#nullable disable -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Games.Common; - -public class GirlRating -{ - public double Crazy { get; } - public double Hot { get; } - public int Roll { get; } - public string Advice { get; } - - public AsyncLazy Stream { get; } - private readonly IImageCache _images; - - public GirlRating( - IImageCache images, - double crazy, - double hot, - int roll, - string advice) - { - _images = images; - Crazy = crazy; - Hot = hot; - Roll = roll; - Advice = advice; // convenient to have it here, even though atm there are only few different ones. - - Stream = new(async () => - { - try - { - var bgBytes = await _images.GetRategirlBgAsync(); - using var img = Image.Load(bgBytes); - const int minx = 35; - const int miny = 385; - const int length = 345; - - var pointx = (int)(minx + (length * (Hot / 10))); - var pointy = (int)(miny - (length * ((Crazy - 4) / 6))); - - var dotBytes = await _images.GetRategirlDotAsync(); - using (var pointImg = Image.Load(dotBytes)) - { - img.Mutate(x => x.DrawImage(pointImg, new(pointx - 10, pointy - 10), new GraphicsOptions())); - } - - var imgStream = new MemoryStream(); - img.SaveAsPng(imgStream); - return imgStream; - } - catch (Exception ex) - { - Log.Warning(ex, "Error getting RateGirl image"); - return null; - } - }); - } -} diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/DefaultHangmanSource.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/DefaultHangmanSource.cs deleted file mode 100644 index 16c400b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/DefaultHangmanSource.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Ellie.Common.Yml; -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Games.Hangman; - -public sealed class DefaultHangmanSource : IHangmanSource -{ - private IReadOnlyDictionary termsDict = new Dictionary(); - private readonly Random _rng; - - public DefaultHangmanSource() - { - _rng = new EllieRandom(); - Reload(); - } - - public void Reload() - { - if (!Directory.Exists("data/hangman")) - { - Log.Error("Hangman game won't work. Folder 'data/hangman' is missing"); - return; - } - - var qs = new Dictionary(); - foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml")) - { - try - { - var data = Yaml.Deserializer.Deserialize(File.ReadAllText(file)); - qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data; - } - catch (Exception ex) - { - Log.Error(ex, "Loading {HangmanFile} failed", file); - } - } - - termsDict = qs; - - Log.Information("Loaded {HangmanCategoryCount} hangman categories", qs.Count); - } - - public IReadOnlyCollection GetCategories() - => termsDict.Keys.ToList(); - - public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term) - { - if (category is null) - { - var cats = GetCategories(); - category = cats.ElementAt(_rng.Next(0, cats.Count)); - } - - if (termsDict.TryGetValue(category, out var terms)) - { - term = terms[_rng.Next(0, terms.Length)]; - return true; - } - - term = null; - return false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanCommands.cs deleted file mode 100644 index 624dccf..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanCommands.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Ellie.Modules.Games.Hangman; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class HangmanCommands : EllieModule - { - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Hangmanlist() - => await SendConfirmAsync(GetText(strs.hangman_types(prefix)), _service.GetHangmanTypes().Join('\n')); - - private static string Draw(HangmanGame.State state) - => $""" - . ┌─────┐ - .┃...............┋ - .┃...............┋ - .┃{(state.Errors > 0 ? ".............😲" : "")} - .┃{(state.Errors > 1 ? "............./" : "")} {(state.Errors > 2 ? "|" : "")} {(state.Errors > 3 ? "\\" : "")} - .┃{(state.Errors > 4 ? "............../" : "")} {(state.Errors > 5 ? "\\" : "")} - /-\ - """; - - public static IEmbedBuilder GetEmbed(IEmbedBuilderService eb, HangmanGame.State state) - { - if (state.Phase == HangmanGame.Phase.Running) - { - return eb.Create() - .WithOkColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.MissedLetters.Join(' ')); - } - - if (state.Phase == HangmanGame.Phase.Ended && state.Failed) - { - return eb.Create() - .WithErrorColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.MissedLetters.Join(' ')); - } - - return eb.Create() - .WithOkColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.MissedLetters.Join(' ')); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Hangman([Leftover] string? type = null) - { - if (!_service.StartHangman(ctx.Channel.Id, type, out var hangman)) - { - await ReplyErrorLocalizedAsync(strs.hangman_running); - return; - } - - var eb = GetEmbed(_eb, hangman); - eb.WithDescription(GetText(strs.hangman_game_started)); - await ctx.Channel.EmbedAsync(eb); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task HangmanStop() - { - if (await _service.StopHangman(ctx.Channel.Id)) - await ReplyConfirmLocalizedAsync(strs.hangman_stopped); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanGame.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanGame.cs deleted file mode 100644 index dabe62e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanGame.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable disable - -namespace Ellie.Modules.Games.Hangman; - -public sealed class HangmanGame -{ - public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win } - - public enum Phase { Running, Ended } - - private Phase CurrentPhase { get; set; } - - private readonly HashSet _incorrect = new(); - private readonly HashSet _correct = new(); - private readonly HashSet _remaining = new(); - - private readonly string _word; - private readonly string _imageUrl; - - public HangmanGame(HangmanTerm term) - { - _word = term.Word; - _imageUrl = term.ImageUrl; - - _remaining = _word.ToLowerInvariant().Where(x => char.IsLetter(x)).Select(char.ToLowerInvariant).ToHashSet(); - } - - public State GetState(GuessResult guessResult = GuessResult.NoAction) - => new(_incorrect.Count, - CurrentPhase, - CurrentPhase == Phase.Ended ? _word : GetScrambledWord(), - guessResult, - _incorrect.ToList(), - CurrentPhase == Phase.Ended ? _imageUrl : string.Empty); - - private string GetScrambledWord() - { - Span output = stackalloc char[_word.Length * 2]; - for (var i = 0; i < _word.Length; i++) - { - var ch = _word[i]; - if (ch == ' ') - output[i * 2] = ' '; - if (!char.IsLetter(ch) || !_remaining.Contains(char.ToLowerInvariant(ch))) - output[i * 2] = ch; - else - output[i * 2] = '_'; - - output[(i * 2) + 1] = ' '; - } - - return new(output); - } - - public State Guess(string guess) - { - if (CurrentPhase != Phase.Running) - return GetState(); - - guess = guess.Trim(); - if (guess.Length > 1) - { - if (guess.Equals(_word, StringComparison.InvariantCultureIgnoreCase)) - { - CurrentPhase = Phase.Ended; - return GetState(GuessResult.Win); - } - - return GetState(); - } - - var charGuess = guess[0]; - if (!char.IsLetter(charGuess)) - return GetState(); - - if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess)) - return GetState(GuessResult.AlreadyTried); - - if (_remaining.Remove(charGuess)) - { - if (_remaining.Count == 0) - { - CurrentPhase = Phase.Ended; - return GetState(GuessResult.Win); - } - - _correct.Add(charGuess); - return GetState(GuessResult.Guess); - } - - _incorrect.Add(charGuess); - if (_incorrect.Count > 5) - { - CurrentPhase = Phase.Ended; - return GetState(GuessResult.Incorrect); - } - - return GetState(GuessResult.Incorrect); - } - - public record State( - int Errors, - Phase Phase, - string Word, - GuessResult GuessResult, - List MissedLetters, - string ImageUrl) - { - public bool Failed - => Errors > 5; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanService.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanService.cs deleted file mode 100644 index 4383b81..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanService.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using Ellie.Common.ModuleBehaviors; -using Ellie.Modules.Games.Services; -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Games.Hangman; - -public sealed class HangmanService : IHangmanService, IExecNoCommand -{ - private readonly ConcurrentDictionary _hangmanGames = new(); - private readonly IHangmanSource _source; - private readonly IEmbedBuilderService _eb; - private readonly GamesConfigService _gcs; - private readonly ICurrencyService _cs; - private readonly IMemoryCache _cdCache; - private readonly object _locker = new(); - - public HangmanService( - IHangmanSource source, - IEmbedBuilderService eb, - GamesConfigService gcs, - ICurrencyService cs, - IMemoryCache cdCache) - { - _source = source; - _eb = eb; - _gcs = gcs; - _cs = cs; - _cdCache = cdCache; - } - - public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state) - { - state = null; - if (!_source.GetTerm(category, out var term)) - return false; - - - var game = new HangmanGame(term); - lock (_locker) - { - var hc = _hangmanGames.GetOrAdd(channelId, game); - if (hc == game) - { - state = hc.GetState(); - return true; - } - - return false; - } - } - - public ValueTask StopHangman(ulong channelId) - { - lock (_locker) - { - if (_hangmanGames.TryRemove(channelId, out _)) - return new(true); - } - - return new(false); - } - - public IReadOnlyCollection GetHangmanTypes() - => _source.GetCategories(); - - public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - { - if (_hangmanGames.ContainsKey(msg.Channel.Id)) - { - if (string.IsNullOrWhiteSpace(msg.Content)) - return; - - if (_cdCache.TryGetValue(msg.Author.Id, out _)) - return; - - HangmanGame.State state; - long rew = 0; - lock (_locker) - { - if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game)) - return; - - state = game.Guess(msg.Content.ToLowerInvariant()); - - if (state.GuessResult == HangmanGame.GuessResult.NoAction) - return; - - if (state.GuessResult is HangmanGame.GuessResult.Incorrect or HangmanGame.GuessResult.AlreadyTried) - { - _cdCache.Set(msg.Author.Id, - string.Empty, - new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3) - }); - } - - if (state.Phase == HangmanGame.Phase.Ended) - { - if (_hangmanGames.TryRemove(msg.Channel.Id, out _)) - rew = _gcs.Data.Hangman.CurrencyReward; - } - } - - if (rew > 0) - await _cs.AddAsync(msg.Author, rew, new("hangman", "win")); - - await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state); - } - } - - private Task SendState( - ITextChannel channel, - IUser user, - string content, - HangmanGame.State state) - { - var embed = Games.HangmanCommands.GetEmbed(_eb, state); - if (state.GuessResult == HangmanGame.GuessResult.Guess) - embed.WithDescription($"{user} guessed the letter {content}!").WithOkColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed) - embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!").WithErrorColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Incorrect) - embed.WithDescription($"{user} Letter {content} doesn't exist!").WithErrorColor(); - else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried) - embed.WithDescription($"{user} Letter {content} has already been used.").WithPendingColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Win) - embed.WithDescription($"{user} won!").WithOkColor(); - - if (!string.IsNullOrWhiteSpace(state.ImageUrl) && Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute)) - embed.WithImageUrl(state.ImageUrl); - - return channel.EmbedAsync(embed); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanTerm.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanTerm.cs deleted file mode 100644 index ce24982..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/HangmanTerm.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Hangman; - -public sealed class HangmanTerm -{ - public string Word { get; set; } - public string ImageUrl { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanService.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanService.cs deleted file mode 100644 index bd603eb..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Games.Hangman; - -public interface IHangmanService -{ - bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController); - ValueTask StopHangman(ulong channelId); - IReadOnlyCollection GetHangmanTypes(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanSource.cs b/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanSource.cs deleted file mode 100644 index c5c9c5c..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Hangman/IHangmanSource.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Games.Hangman; - -public interface IHangmanSource : IEService -{ - public IReadOnlyCollection GetCategories(); - public void Reload(); - public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/Nunchi.cs b/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/Nunchi.cs deleted file mode 100644 index 8dcd787..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/Nunchi.cs +++ /dev/null @@ -1,183 +0,0 @@ -#nullable disable -using System.Collections.Immutable; - -namespace Ellie.Modules.Games.Common.Nunchi; - -public sealed class NunchiGame : IDisposable -{ - public enum Phase - { - Joining, - Playing, - WaitingForNextRound, - Ended - } - - private const int KILL_TIMEOUT = 20 * 1000; - private const int NEXT_ROUND_TIMEOUT = 5 * 1000; - - public event Func OnGameStarted; - public event Func OnRoundStarted; - public event Func OnUserGuessed; - public event Func OnRoundEnded; // tuple of the user who failed - public event Func OnGameEnded; // name of the user who won - - public int CurrentNumber { get; private set; } = new EllieRandom().Next(0, 100); - public Phase CurrentPhase { get; private set; } = Phase.Joining; - - public ImmutableArray<(ulong Id, string Name)> Participants - => participants.ToImmutableArray(); - - public int ParticipantCount - => participants.Count; - - private readonly SemaphoreSlim _locker = new(1, 1); - - private HashSet<(ulong Id, string Name)> participants = new(); - private readonly HashSet<(ulong Id, string Name)> _passed = new(); - private Timer killTimer; - - public NunchiGame(ulong creatorId, string creatorName) - => participants.Add((creatorId, creatorName)); - - public async Task Join(ulong userId, string userName) - { - await _locker.WaitAsync(); - try - { - if (CurrentPhase != Phase.Joining) - return false; - - return participants.Add((userId, userName)); - } - finally { _locker.Release(); } - } - - public async Task Initialize() - { - CurrentPhase = Phase.Joining; - await Task.Delay(30000); - await _locker.WaitAsync(); - try - { - if (participants.Count < 3) - { - CurrentPhase = Phase.Ended; - return false; - } - - killTimer = new(async _ => - { - await _locker.WaitAsync(); - try - { - if (CurrentPhase != Phase.Playing) - return; - - //if some players took too long to type a number, boot them all out and start a new round - participants = new HashSet<(ulong, string)>(_passed); - EndRound(); - } - finally { _locker.Release(); } - }, - null, - KILL_TIMEOUT, - KILL_TIMEOUT); - - CurrentPhase = Phase.Playing; - _ = OnGameStarted?.Invoke(this); - _ = OnRoundStarted?.Invoke(this, CurrentNumber); - return true; - } - finally { _locker.Release(); } - } - - public async Task Input(ulong userId, string userName, int input) - { - await _locker.WaitAsync(); - try - { - if (CurrentPhase != Phase.Playing) - return; - - var userTuple = (Id: userId, Name: userName); - - // if the user is not a member of the race, - // or he already successfully typed the number - // ignore the input - if (!participants.Contains(userTuple) || !_passed.Add(userTuple)) - return; - - //if the number is correct - if (CurrentNumber == input - 1) - { - //increment current number - ++CurrentNumber; - if (_passed.Count == participants.Count - 1) - { - // if only n players are left, and n - 1 type the correct number, round is over - - // if only 2 players are left, game is over - if (participants.Count == 2) - { - killTimer.Change(Timeout.Infinite, Timeout.Infinite); - CurrentPhase = Phase.Ended; - _ = OnGameEnded?.Invoke(this, userTuple.Name); - } - else // else just start the new round without the user who was the last - { - var failure = participants.Except(_passed).First(); - - OnUserGuessed?.Invoke(this); - EndRound(failure); - return; - } - } - - OnUserGuessed?.Invoke(this); - } - else - { - //if the user failed - - EndRound(userTuple); - } - } - finally { _locker.Release(); } - } - - private void EndRound((ulong, string)? failure = null) - { - killTimer.Change(KILL_TIMEOUT, KILL_TIMEOUT); - CurrentNumber = new EllieRandom().Next(0, 100); // reset the counter - _passed.Clear(); // reset all users who passed (new round starts) - if (failure is not null) - participants.Remove(failure.Value); // remove the dude who failed from the list of players - - _ = OnRoundEnded?.Invoke(this, failure); - if (participants.Count <= 1) // means we have a winner or everyone was booted out - { - killTimer.Change(Timeout.Infinite, Timeout.Infinite); - CurrentPhase = Phase.Ended; - _ = OnGameEnded?.Invoke(this, participants.Count > 0 ? participants.First().Name : null); - return; - } - - CurrentPhase = Phase.WaitingForNextRound; - Task.Run(async () => - { - await Task.Delay(NEXT_ROUND_TIMEOUT); - CurrentPhase = Phase.Playing; - _ = OnRoundStarted?.Invoke(this, CurrentNumber); - }); - } - - public void Dispose() - { - OnGameEnded = null; - OnGameStarted = null; - OnRoundEnded = null; - OnRoundStarted = null; - OnUserGuessed = null; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/NunchiCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/NunchiCommands.cs deleted file mode 100644 index fa92b97..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Nunchi/NunchiCommands.cs +++ /dev/null @@ -1,111 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common.Nunchi; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class NunchiCommands : EllieModule - { - private readonly DiscordSocketClient _client; - - public NunchiCommands(DiscordSocketClient client) - => _client = client; - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Nunchi() - { - var newNunchi = new NunchiGame(ctx.User.Id, ctx.User.ToString()); - NunchiGame nunchi; - - //if a game was already active - if ((nunchi = _service.NunchiGames.GetOrAdd(ctx.Guild.Id, newNunchi)) != newNunchi) - { - // join it - if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString())) - // if you failed joining, that means game is running or just ended - // await ReplyErrorLocalized("nunchi_already_started"); - return; - - await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount)); - return; - } - - - try { await ConfirmLocalizedAsync(strs.nunchi_created); } - catch { } - - nunchi.OnGameEnded += NunchiOnGameEnded; - //nunchi.OnGameStarted += Nunchi_OnGameStarted; - nunchi.OnRoundEnded += Nunchi_OnRoundEnded; - nunchi.OnUserGuessed += Nunchi_OnUserGuessed; - nunchi.OnRoundStarted += Nunchi_OnRoundStarted; - _client.MessageReceived += ClientMessageReceived; - - var success = await nunchi.Initialize(); - if (!success) - { - if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) - game.Dispose(); - await ConfirmLocalizedAsync(strs.nunchi_failed_to_start); - } - - Task ClientMessageReceived(SocketMessage arg) - { - _ = Task.Run(async () => - { - if (arg.Channel.Id != ctx.Channel.Id) - return; - - if (!int.TryParse(arg.Content, out var number)) - return; - try - { - await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number); - } - catch - { - } - }); - return Task.CompletedTask; - } - - Task NunchiOnGameEnded(NunchiGame arg1, string arg2) - { - if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) - { - _client.MessageReceived -= ClientMessageReceived; - game.Dispose(); - } - - if (arg2 is null) - return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner); - return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2))); - } - } - - private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur) - => ConfirmLocalizedAsync(strs.nunchi_round_started(Format.Bold(arg.ParticipantCount.ToString()), - Format.Bold(cur.ToString()))); - - private Task Nunchi_OnUserGuessed(NunchiGame arg) - => ConfirmLocalizedAsync(strs.nunchi_next_number(Format.Bold(arg.CurrentNumber.ToString()))); - - private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2) - { - if (arg2.HasValue) - return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name))); - return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot( - Format.Bold("\n" - + string.Join("\n, ", - arg1.Participants.Select(x - => x.Name))))); // this won't work if there are too many users - } - - private Task Nunchi_OnGameStarted(NunchiGame arg) - => ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString()))); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollCommands.cs deleted file mode 100644 index 1d3d06b..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollCommands.cs +++ /dev/null @@ -1,102 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Services; -using Ellie.Services.Database.Models; -using System.Text; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class PollCommands : EllieModule - { - private readonly DiscordSocketClient _client; - - public PollCommands(DiscordSocketClient client) - => _client = client; - - [Cmd] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task Poll([Leftover] string arg) - { - if (string.IsNullOrWhiteSpace(arg)) - return; - - var poll = _service.CreatePoll(ctx.Guild.Id, ctx.Channel.Id, arg); - if (poll is null) - { - await ReplyErrorLocalizedAsync(strs.poll_invalid_input); - return; - } - - if (_service.StartPoll(poll)) - { - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.poll_created(ctx.User.ToString()))) - .WithDescription(Format.Bold(poll.Question) - + "\n\n" - + string.Join("\n", - poll.Answers.Select(x - => $"`{x.Index + 1}.` {Format.Bold(x.Text)}")))); - } - else - await ReplyErrorLocalizedAsync(strs.poll_already_running); - } - - [Cmd] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task PollStats() - { - if (!_service.ActivePolls.TryGetValue(ctx.Guild.Id, out var pr)) - return; - - await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results))); - } - - [Cmd] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task Pollend() - { - Poll p; - if ((p = _service.StopPoll(ctx.Guild.Id)) is null) - return; - - var embed = GetStats(p, GetText(strs.poll_closed)); - await ctx.Channel.EmbedAsync(embed); - } - - public IEmbedBuilder GetStats(Poll poll, string title) - { - var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex).ToDictionary(x => x.Key, x => x.Sum(_ => 1)); - - var totalVotesCast = results.Sum(x => x.Value); - - var eb = _eb.Create().WithTitle(title); - - var sb = new StringBuilder().AppendLine(Format.Bold(poll.Question)).AppendLine(); - - var stats = poll.Answers.Select(x => - { - results.TryGetValue(x.Index, out var votes); - - return (x.Index, votes, x.Text); - }) - .OrderByDescending(x => x.votes) - .ToArray(); - - for (var i = 0; i < stats.Length; i++) - { - var (index, votes, text) = stats[i]; - sb.AppendLine(GetText(strs.poll_result(index + 1, Format.Bold(text), Format.Bold(votes.ToString())))); - } - - return eb.WithDescription(sb.ToString()) - .WithFooter(GetText(strs.x_votes_cast(totalVotesCast))) - .WithOkColor(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollExtensions.cs b/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollExtensions.cs deleted file mode 100644 index 13a4f90..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class PollExtensions -{ - public static IEnumerable GetAllPolls(this DbSet polls) - => polls.Include(x => x.Answers) - .Include(x => x.Votes) - .ToArray(); - - public static void RemovePoll(this DbContext ctx, int id) - { - var p = ctx.Set().Include(x => x.Answers).Include(x => x.Votes).FirstOrDefault(x => x.Id == id); - - if (p is null) - return; - - if (p.Votes is not null) - { - ctx.RemoveRange(p.Votes); - p.Votes.Clear(); - } - - if (p.Answers is not null) - { - ctx.RemoveRange(p.Answers); - p.Answers.Clear(); - } - - ctx.Set().Remove(p); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollRunner.cs b/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollRunner.cs deleted file mode 100644 index 2f80d8e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollRunner.cs +++ /dev/null @@ -1,63 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Games.Common; - -public class PollRunner -{ - public event Func OnVoted; - public Poll Poll { get; } - private readonly DbService _db; - - private readonly SemaphoreSlim _locker = new(1, 1); - - public PollRunner(DbService db, Poll poll) - { - _db = db; - Poll = poll; - } - - public async Task TryVote(IUserMessage msg) - { - PollVote voteObj; - await _locker.WaitAsync(); - try - { - // has to be a user message - // channel must be the same the poll started in - if (msg is null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId) - return false; - - // has to be an integer - if (!int.TryParse(msg.Content, out var vote)) - return false; - --vote; - if (vote < 0 || vote >= Poll.Answers.Count) - return false; - - var usr = msg.Author as IGuildUser; - if (usr is null) - return false; - - voteObj = new() - { - UserId = msg.Author.Id, - VoteIndex = vote - }; - if (!Poll.Votes.Add(voteObj)) - return false; - - _ = OnVoted?.Invoke(msg, usr); - } - finally { _locker.Release(); } - - await using var uow = _db.GetDbContext(); - var trackedPoll = uow.Set().FirstOrDefault(x => x.Id == Poll.Id); - trackedPoll.Votes.Add(voteObj); - uow.SaveChanges(); - return true; - } - - public void End() - => OnVoted = null; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollService.cs b/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollService.cs deleted file mode 100644 index 8b066d6..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Polls/PollService.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable disable -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Modules.Games.Common; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Games.Services; - -public class PollService : IExecOnMessage -{ - public ConcurrentDictionary ActivePolls { get; } = new(); - - public int Priority - => 5; - - private readonly DbService _db; - private readonly IBotStrings _strs; - private readonly IEmbedBuilderService _eb; - - public PollService(DbService db, IBotStrings strs, IEmbedBuilderService eb) - { - _db = db; - _strs = strs; - _eb = eb; - - using var uow = db.GetDbContext(); - ActivePolls = uow.Set().GetAllPolls() - .ToDictionary(x => x.GuildId, - x => - { - var pr = new PollRunner(db, x); - pr.OnVoted += Pr_OnVoted; - return pr; - }) - .ToConcurrent(); - } - - public Poll CreatePoll(ulong guildId, ulong channelId, string input) - { - if (string.IsNullOrWhiteSpace(input) || !input.Contains(";")) - return null; - var data = input.Split(';'); - if (data.Length < 3) - return null; - - var col = new IndexedCollection(data.Skip(1) - .Select(x => new PollAnswer - { - Text = x - })); - - return new() - { - Answers = col, - Question = data[0], - ChannelId = channelId, - GuildId = guildId, - Votes = new() - }; - } - - public bool StartPoll(Poll p) - { - var pr = new PollRunner(_db, p); - if (ActivePolls.TryAdd(p.GuildId, pr)) - { - using (var uow = _db.GetDbContext()) - { - uow.Set().Add(p); - uow.SaveChanges(); - } - - pr.OnVoted += Pr_OnVoted; - return true; - } - - return false; - } - - public Poll StopPoll(ulong guildId) - { - if (ActivePolls.TryRemove(guildId, out var pr)) - { - pr.OnVoted -= Pr_OnVoted; - - using var uow = _db.GetDbContext(); - uow.RemovePoll(pr.Poll.Id); - uow.SaveChanges(); - - return pr.Poll; - } - - return null; - } - - private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr) - { - var toDelete = await msg.Channel.SendConfirmAsync(_eb, - _strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId)); - toDelete.DeleteAfter(5); - try - { - await msg.DeleteAsync(); - } - catch - { - } - } - - public async Task ExecOnMessageAsync(IGuild guild, IUserMessage msg) - { - if (guild is null) - return false; - - if (!ActivePolls.TryGetValue(guild.Id, out var poll)) - return false; - - try - { - var voted = await poll.TryVote(msg); - - if (voted) - { - Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server", - msg.Author.ToString(), - msg.Author.Id, - guild.Name, - guild.Id); - } - - return voted; - } - catch (Exception ex) - { - Log.Warning(ex, "Error voting"); - } - - return false; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/SpeedTypingCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/SpeedTypingCommands.cs deleted file mode 100644 index 147d0ba..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/SpeedTypingCommands.cs +++ /dev/null @@ -1,103 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class SpeedTypingCommands : EllieModule - { - private readonly GamesService _games; - private readonly DiscordSocketClient _client; - - public SpeedTypingCommands(DiscordSocketClient client, GamesService games) - { - _games = games; - _client = client; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public async Task TypeStart(params string[] args) - { - var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args); - var channel = (ITextChannel)ctx.Channel; - - var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, - _ => new(_games, _client, channel, prefix, options, _eb)); - - if (game.IsActive) - await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel."); - else - await game.Start(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task TypeStop() - { - if (_service.RunningContests.TryRemove(ctx.Guild.Id, out var game)) - { - await game.Stop(); - return; - } - - await SendErrorAsync("No contest to stop on this channel."); - } - - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Typeadd([Leftover] string text) - { - if (string.IsNullOrWhiteSpace(text)) - return; - - _games.AddTypingArticle(ctx.User, text); - - await SendConfirmAsync("Added new article for typing game."); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Typelist(int page = 1) - { - if (page < 1) - return; - - var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray(); - - if (!articles.Any()) - { - await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`"); - return; - } - - var i = (page - 1) * 15; - await SendConfirmAsync("List of articles for Type Race", - string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}"))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Typedel(int index) - { - var removed = _service.RemoveTypingArticle(--index); - - if (removed is null) - return; - - var embed = _eb.Create() - .WithTitle($"Removed typing article #{index + 1}") - .WithDescription(removed.Text.TrimTo(50)) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingArticle.cs b/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingArticle.cs deleted file mode 100644 index bc144f6..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingArticle.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Games.Common; - -public class TypingArticle -{ - public string Source { get; set; } - public string Extra { get; set; } - public string Text { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingGame.cs b/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingGame.cs deleted file mode 100644 index e787139..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/SpeedTyping/TypingGame.cs +++ /dev/null @@ -1,183 +0,0 @@ -#nullable disable -using CommandLine; -using Ellie.Modules.Games.Services; -using System.Diagnostics; - -namespace Ellie.Modules.Games.Common; - -public class TypingGame -{ - public const float WORD_VALUE = 4.5f; - public ITextChannel Channel { get; } - public string CurrentSentence { get; private set; } - public bool IsActive { get; private set; } - private readonly Stopwatch _sw; - private readonly List _finishedUserIds; - private readonly DiscordSocketClient _client; - private readonly GamesService _games; - private readonly string _prefix; - private readonly Options _options; - private readonly IEmbedBuilderService _eb; - - public TypingGame( - GamesService games, - DiscordSocketClient client, - ITextChannel channel, - string prefix, - Options options, - IEmbedBuilderService eb) - { - _games = games; - _client = client; - _prefix = prefix; - _options = options; - _eb = eb; - - Channel = channel; - IsActive = false; - _sw = new(); - _finishedUserIds = new(); - } - - public async Task Stop() - { - if (!IsActive) - return false; - _client.MessageReceived -= AnswerReceived; - _finishedUserIds.Clear(); - IsActive = false; - _sw.Stop(); - _sw.Reset(); - try - { - await Channel.SendConfirmAsync(_eb, "Typing contest stopped."); - } - catch - { - } - - return true; - } - - public async Task Start() - { - if (IsActive) - return; // can't start running game - IsActive = true; - CurrentSentence = GetRandomSentence(); - var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f); - try - { - await Channel.SendConfirmAsync(_eb, - $":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can."); - - - var time = _options.StartTime; - - var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**..."); - - do - { - await Task.Delay(2000); - time -= 2; - try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**.."); } - catch { } - } while (time > 2); - - await msg.ModifyAsync(m => - { - m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture); - }); - _sw.Start(); - HandleAnswers(); - - while (i > 0) - { - await Task.Delay(1000); - i--; - if (!IsActive) - return; - } - } - catch { } - finally - { - await Stop(); - } - } - - public string GetRandomSentence() - { - if (_games.TypingArticles.Any()) - return _games.TypingArticles[new EllieRandom().Next(0, _games.TypingArticles.Count)].Text; - return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing."; - } - - private void HandleAnswers() - => _client.MessageReceived += AnswerReceived; - - private Task AnswerReceived(SocketMessage imsg) - { - _ = Task.Run(async () => - { - try - { - if (imsg.Author.IsBot) - return; - if (imsg is not SocketUserMessage msg) - return; - - if (Channel is null || Channel.Id != msg.Channel.Id) - return; - - var guess = msg.Content; - - var distance = CurrentSentence.LevenshteinDistance(guess); - var decision = Judge(distance, guess.Length); - if (decision && !_finishedUserIds.Contains(msg.Author.Id)) - { - var elapsed = _sw.Elapsed; - var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60; - _finishedUserIds.Add(msg.Author.Id); - await Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle($"{msg.Author} finished the race!") - .AddField("Place", $"#{_finishedUserIds.Count}", true) - .AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true) - .AddField("Errors", distance.ToString(), true)); - - if (_finishedUserIds.Count % 4 == 0) - { - await Channel.SendConfirmAsync(_eb, - ":exclamation: A lot of people finished, here is the text for those still typing:" - + $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**"); - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error receiving typing game answer: {ErrorMessage}", ex.Message); - } - }); - return Task.CompletedTask; - } - - private static bool Judge(int errors, int textLength) - => errors <= textLength / 25; - - public class Options : IEllieCommandOptions - { - [Option('s', - "start-time", - Default = 5, - Required = false, - HelpText = "How long does it take for the race to start. Default 5.")] - public int StartTime { get; set; } = 5; - - public void NormalizeOptions() - { - if (StartTime is < 3 or > 30) - StartTime = 5; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToe.cs b/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToe.cs deleted file mode 100644 index 1d4075e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToe.cs +++ /dev/null @@ -1,307 +0,0 @@ -#nullable disable -using CommandLine; -using System.Text; - -namespace Ellie.Modules.Games.Common; - -public class TicTacToe -{ - public event Action OnEnded; - private readonly ITextChannel _channel; - private readonly IGuildUser[] _users; - private readonly int?[,] _state; - private Phase phase; - private int curUserIndex; - private readonly SemaphoreSlim _moveLock; - - private IGuildUser winner; - - private readonly string[] _numbers = - { - ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" - }; - - private IUserMessage previousMessage; - private Timer timeoutTimer; - private readonly IBotStrings _strings; - private readonly DiscordSocketClient _client; - private readonly Options _options; - private readonly IEmbedBuilderService _eb; - - public TicTacToe( - IBotStrings strings, - DiscordSocketClient client, - ITextChannel channel, - IGuildUser firstUser, - Options options, - IEmbedBuilderService eb) - { - _channel = channel; - _strings = strings; - _client = client; - _options = options; - _eb = eb; - - _users = new[] { firstUser, null }; - _state = new int?[,] { { null, null, null }, { null, null, null }, { null, null, null } }; - - phase = Phase.Starting; - _moveLock = new(1, 1); - } - - private string GetText(LocStr key) - => _strings.GetText(key, _channel.GuildId); - - public string GetState() - { - var sb = new StringBuilder(); - for (var i = 0; i < _state.GetLength(0); i++) - { - for (var j = 0; j < _state.GetLength(1); j++) - { - sb.Append(_state[i, j] is null ? _numbers[(i * 3) + j] : GetIcon(_state[i, j])); - if (j < _state.GetLength(1) - 1) - sb.Append("┃"); - } - - if (i < _state.GetLength(0) - 1) - sb.AppendLine("\n──────────"); - } - - return sb.ToString(); - } - - public IEmbedBuilder GetEmbed(string title = null) - { - var embed = _eb.Create() - .WithOkColor() - .WithDescription(Environment.NewLine + GetState()) - .WithAuthor(GetText(strs.vs(_users[0], _users[1]))); - - if (!string.IsNullOrWhiteSpace(title)) - embed.WithTitle(title); - - if (winner is null) - { - if (phase == Phase.Ended) - embed.WithFooter(GetText(strs.ttt_no_moves)); - else - embed.WithFooter(GetText(strs.ttt_users_move(_users[curUserIndex]))); - } - else - embed.WithFooter(GetText(strs.ttt_has_won(winner))); - - return embed; - } - - private static string GetIcon(int? val) - { - switch (val) - { - case 0: - return "❌"; - case 1: - return "⭕"; - case 2: - return "❎"; - case 3: - return "🅾"; - default: - return "⬛"; - } - } - - public async Task Start(IGuildUser user) - { - if (phase is Phase.Started or Phase.Ended) - { - await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_already_running)); - return; - } - - if (_users[0] == user) - { - await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_against_yourself)); - return; - } - - _users[1] = user; - - phase = Phase.Started; - - timeoutTimer = new(async _ => - { - await _moveLock.WaitAsync(); - try - { - if (phase == Phase.Ended) - return; - - phase = Phase.Ended; - if (_users[1] is not null) - { - winner = _users[curUserIndex ^= 1]; - var del = previousMessage?.DeleteAsync(); - try - { - await _channel.EmbedAsync(GetEmbed(GetText(strs.ttt_time_expired))); - if (del is not null) - await del; - } - catch { } - } - - OnEnded?.Invoke(this); - } - catch { } - finally - { - _moveLock.Release(); - } - }, - null, - _options.TurnTimer * 1000, - Timeout.Infinite); - - _client.MessageReceived += Client_MessageReceived; - - - previousMessage = await _channel.EmbedAsync(GetEmbed(GetText(strs.game_started))); - } - - private bool IsDraw() - { - for (var i = 0; i < 3; i++) - for (var j = 0; j < 3; j++) - { - if (_state[i, j] is null) - return false; - } - - return true; - } - - private Task Client_MessageReceived(SocketMessage msg) - { - _ = Task.Run(async () => - { - await _moveLock.WaitAsync(); - try - { - var curUser = _users[curUserIndex]; - if (phase == Phase.Ended || msg.Author?.Id != curUser.Id) - return; - - if (int.TryParse(msg.Content, out var index) - && --index >= 0 - && index <= 9 - && _state[index / 3, index % 3] is null) - { - _state[index / 3, index % 3] = curUserIndex; - - // i'm lazy - if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2]) - { - _state[index / 3, 0] = curUserIndex + 2; - _state[index / 3, 1] = curUserIndex + 2; - _state[index / 3, 2] = curUserIndex + 2; - - phase = Phase.Ended; - } - else if (_state[0, index % 3] == _state[1, index % 3] - && _state[1, index % 3] == _state[2, index % 3]) - { - _state[0, index % 3] = curUserIndex + 2; - _state[1, index % 3] = curUserIndex + 2; - _state[2, index % 3] = curUserIndex + 2; - - phase = Phase.Ended; - } - else if (curUserIndex == _state[0, 0] - && _state[0, 0] == _state[1, 1] - && _state[1, 1] == _state[2, 2]) - { - _state[0, 0] = curUserIndex + 2; - _state[1, 1] = curUserIndex + 2; - _state[2, 2] = curUserIndex + 2; - - phase = Phase.Ended; - } - else if (curUserIndex == _state[0, 2] - && _state[0, 2] == _state[1, 1] - && _state[1, 1] == _state[2, 0]) - { - _state[0, 2] = curUserIndex + 2; - _state[1, 1] = curUserIndex + 2; - _state[2, 0] = curUserIndex + 2; - - phase = Phase.Ended; - } - - var reason = string.Empty; - - if (phase == Phase.Ended) // if user won, stop receiving moves - { - reason = GetText(strs.ttt_matched_three); - winner = _users[curUserIndex]; - _client.MessageReceived -= Client_MessageReceived; - OnEnded?.Invoke(this); - } - else if (IsDraw()) - { - reason = GetText(strs.ttt_a_draw); - phase = Phase.Ended; - _client.MessageReceived -= Client_MessageReceived; - OnEnded?.Invoke(this); - } - - _ = Task.Run(async () => - { - var del1 = msg.DeleteAsync(); - var del2 = previousMessage?.DeleteAsync(); - try { previousMessage = await _channel.EmbedAsync(GetEmbed(reason)); } - catch { } - - try { await del1; } - catch { } - - try - { - if (del2 is not null) - await del2; - } - catch { } - }); - curUserIndex ^= 1; - - timeoutTimer.Change(_options.TurnTimer * 1000, Timeout.Infinite); - } - } - finally - { - _moveLock.Release(); - } - }); - - return Task.CompletedTask; - } - - public class Options : IEllieCommandOptions - { - [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")] - public int TurnTimer { get; set; } = 15; - - public void NormalizeOptions() - { - if (TurnTimer is < 5 or > 60) - TurnTimer = 15; - } - } - - private enum Phase - { - Starting, - Started, - Ended - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToeCommands.cs b/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToeCommands.cs deleted file mode 100644 index 674ebe0..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/TicTacToe/TicTacToeCommands.cs +++ /dev/null @@ -1,54 +0,0 @@ -#nullable disable -using Ellie.Modules.Games.Common; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class TicTacToeCommands : EllieModule - { - private readonly SemaphoreSlim _sem = new(1, 1); - private readonly DiscordSocketClient _client; - - public TicTacToeCommands(DiscordSocketClient client) - => _client = client; - - [Cmd] - [RequireContext(ContextType.Guild)] - [EllieOptions] - public async Task TicTacToe(params string[] args) - { - var (options, _) = OptionsParser.ParseFrom(new TicTacToe.Options(), args); - var channel = (ITextChannel)ctx.Channel; - - await _sem.WaitAsync(1000); - try - { - if (_service.TicTacToeGames.TryGetValue(channel.Id, out var game)) - { - _ = Task.Run(async () => - { - await game.Start((IGuildUser)ctx.User); - }); - return; - } - - game = new(Strings, _client, channel, (IGuildUser)ctx.User, options, _eb); - _service.TicTacToeGames.Add(channel.Id, game); - await ReplyConfirmLocalizedAsync(strs.ttt_created); - - game.OnEnded += _ => - { - _service.TicTacToeGames.Remove(channel.Id); - _sem.Dispose(); - }; - } - finally - { - _sem.Release(); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/Games.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/Games.cs deleted file mode 100644 index f9a8ee1..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/Games.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Net; -using System.Text; -using Ellie.Modules.Games.Common.Trivia; -using Ellie.Modules.Games.Services; - -namespace Ellie.Modules.Games; - -public partial class Games -{ - [Group] - public partial class TriviaCommands : EllieModule - { - private readonly ILocalDataCache _cache; - private readonly ICurrencyService _cs; - private readonly GamesConfigService _gamesConfig; - private readonly DiscordSocketClient _client; - - public TriviaCommands( - DiscordSocketClient client, - ILocalDataCache cache, - ICurrencyService cs, - GamesConfigService gamesConfig) - { - _cache = cache; - _cs = cs; - _gamesConfig = gamesConfig; - _client = client; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - [EllieOptions] - public async Task Trivia(params string[] args) - { - var (opts, _) = OptionsParser.ParseFrom(new TriviaOptions(), args); - - var config = _gamesConfig.Data; - if (opts.WinRequirement != 0 && config.Trivia.MinimumWinReq > 0 && config.Trivia.MinimumWinReq > opts.WinRequirement) - return; - - var trivia = new TriviaGame(opts, _cache); - if (_service.RunningTrivias.TryAdd(ctx.Guild.Id, trivia)) - { - RegisterEvents(trivia); - await trivia.RunAsync(); - return; - } - - if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out var tg)) - { - await SendErrorAsync(GetText(strs.trivia_already_running)); - await tg.TriggerQuestionAsync(); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Tl() - { - if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out var trivia)) - { - await trivia.TriggerStatsAsync(); - return; - } - - await ReplyErrorLocalizedAsync(strs.trivia_none); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Tq() - { - var channel = (ITextChannel)ctx.Channel; - - if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out var trivia)) - { - if (trivia.Stop()) - { - try - { - await ctx.Channel.SendConfirmAsync(_eb, - GetText(strs.trivia_game), - GetText(strs.trivia_stopping)); - } - catch (Exception ex) - { - Log.Warning(ex, "Error sending trivia stopping message"); - } - } - - return; - } - - await ReplyErrorLocalizedAsync(strs.trivia_none); - } - - private string GetLeaderboardString(TriviaGame tg) - { - var sb = new StringBuilder(); - - foreach (var (id, pts) in tg.GetLeaderboard()) - sb.AppendLine(GetText(strs.trivia_points(Format.Bold($"<@{id}>"), pts))); - - return sb.ToString(); - - } - - private IEmbedBuilder? questionEmbed = null; - private IUserMessage? questionMessage = null; - private bool showHowToQuit = false; - - private void RegisterEvents(TriviaGame trivia) - { - trivia.OnQuestion += OnTriviaQuestion; - trivia.OnHint += OnTriviaHint; - trivia.OnGuess += OnTriviaGuess; - trivia.OnEnded += OnTriviaEnded; - trivia.OnStats += OnTriviaStats; - trivia.OnTimeout += OnTriviaTimeout; - } - - private void UnregisterEvents(TriviaGame trivia) - { - trivia.OnQuestion -= OnTriviaQuestion; - trivia.OnHint -= OnTriviaHint; - trivia.OnGuess -= OnTriviaGuess; - trivia.OnEnded -= OnTriviaEnded; - trivia.OnStats -= OnTriviaStats; - trivia.OnTimeout -= OnTriviaTimeout; - } - - private async Task OnTriviaHint(TriviaGame game, TriviaQuestion question) - { - try - { - if (questionMessage is null) - { - game.Stop(); - return; - } - - if (questionEmbed is not null) - await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(question.GetHint()).Build()); - } - catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden) - { - Log.Warning("Unable to edit message to show hint. Stopping trivia"); - game.Stop(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error editing trivia message"); - } - } - - private async Task OnTriviaQuestion(TriviaGame game, TriviaQuestion question) - { - try - { - questionEmbed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.trivia_game)) - .AddField(GetText(strs.category), question.Category) - .AddField(GetText(strs.question), question.Question); - - showHowToQuit = !showHowToQuit; - if (showHowToQuit) - questionEmbed.WithFooter(GetText(strs.trivia_quit($"{prefix}tq"))); - - if (Uri.IsWellFormedUriString(question.ImageUrl, UriKind.Absolute)) - questionEmbed.WithImageUrl(question.ImageUrl); - - questionMessage = await ctx.Channel.EmbedAsync(questionEmbed); - } - catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden - or HttpStatusCode.BadRequest) - { - Log.Warning("Unable to send trivia questions. Stopping immediately"); - game.Stop(); - throw; - } - } - - private async Task OnTriviaTimeout(TriviaGame _, TriviaQuestion question) - { - try - { - var embed = _eb.Create() - .WithErrorColor() - .WithTitle(GetText(strs.trivia_game)) - .WithDescription(GetText(strs.trivia_times_up(Format.Bold(question.Answer)))); - - if (Uri.IsWellFormedUriString(question.AnswerImageUrl, UriKind.Absolute)) - embed.WithImageUrl(question.AnswerImageUrl); - - await ctx.Channel.EmbedAsync(embed); - } - catch - { - // ignored - } - } - - private async Task OnTriviaStats(TriviaGame game) - { - try - { - await SendConfirmAsync(GetText(strs.leaderboard), GetLeaderboardString(game)); - } - catch - { - // ignored - } - } - - private async Task OnTriviaEnded(TriviaGame game) - { - try - { - await ctx.Channel.EmbedAsync(_eb.Create(ctx) - .WithOkColor() - .WithAuthor(GetText(strs.trivia_ended)) - .WithTitle(GetText(strs.leaderboard)) - .WithDescription(GetLeaderboardString(game))); - } - catch - { - // ignored - } - finally - { - _service.RunningTrivias.TryRemove(ctx.Guild.Id, out _); - } - - UnregisterEvents(game); - } - - private async Task OnTriviaGuess(TriviaGame _, TriviaUser user, TriviaQuestion question, bool isWin) - { - try - { - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.trivia_game)) - .WithDescription(GetText(strs.trivia_win(user.Name, - Format.Bold(question.Answer)))); - - if (Uri.IsWellFormedUriString(question.AnswerImageUrl, UriKind.Absolute)) - embed.WithImageUrl(question.AnswerImageUrl); - - - if (isWin) - { - await ctx.Channel.EmbedAsync(embed); - - var reward = _gamesConfig.Data.Trivia.CurrencyReward; - if (reward > 0) - await _cs.AddAsync(user.Id, reward, new("trivia", "win")); - - return; - } - - embed.WithDescription(GetText(strs.trivia_guess(user.Name, - Format.Bold(question.Answer)))); - - await ctx.Channel.EmbedAsync(embed); - } - catch - { - // ignored - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/DefaultQuestionPool.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/DefaultQuestionPool.cs deleted file mode 100644 index 032a95e..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/DefaultQuestionPool.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Ellie.Modules.Games.Common.Trivia; - -public sealed class DefaultQuestionPool : IQuestionPool -{ - private readonly ILocalDataCache _cache; - private readonly EllieRandom _rng; - - public DefaultQuestionPool(ILocalDataCache cache) - { - _cache = cache; - _rng = new EllieRandom(); - } - public async Task GetQuestionAsync() - { - var pool = await _cache.GetTriviaQuestionsAsync(); - - if(pool is null or {Length: 0}) - return default; - - return new(pool[_rng.Next(0, pool.Length)]); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/IQuestionPool.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/IQuestionPool.cs deleted file mode 100644 index e9470f9..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/IQuestionPool.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Modules.Games.Common.Trivia; - -public interface IQuestionPool -{ - Task GetQuestionAsync(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/PokemonQuestionPool.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/PokemonQuestionPool.cs deleted file mode 100644 index 70ca61d..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/QuestionPool/PokemonQuestionPool.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Ellie.Modules.Games.Common.Trivia; - -public sealed class PokemonQuestionPool : IQuestionPool -{ - public int QuestionsCount => 905; // xd - private readonly EllieRandom _rng; - private readonly ILocalDataCache _cache; - - public PokemonQuestionPool(ILocalDataCache cache) - { - _cache = cache; - _rng = new EllieRandom(); - } - - public async Task GetQuestionAsync() - { - var pokes = await _cache.GetPokemonMapAsync(); - - if (pokes is null or { Count: 0 }) - return default; - - var num = _rng.Next(1, QuestionsCount + 1); - return new(new() - { - Question = "Who's That Pokémon?", - Answer = pokes[num].ToTitleCase(), - Category = "Pokemon", - ImageUrl = $@"https://nadeko.bot/images/pokemon/shadows/{num}.png", - AnswerImageUrl = $@"https://nadeko.bot/images/pokemon/real/{num}.png" - }); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGame.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGame.cs deleted file mode 100644 index 234d236..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGame.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System.Threading.Channels; -using Exception = System.Exception; - -namespace Ellie.Modules.Games.Common.Trivia; - -public sealed class TriviaGame -{ - private readonly TriviaOptions _opts; - - - private readonly IQuestionPool _questionPool; - - #region Events - public event Func OnQuestion = static delegate { return Task.CompletedTask; }; - public event Func OnHint = static delegate { return Task.CompletedTask; }; - public event Func OnStats = static delegate { return Task.CompletedTask; }; - public event Func OnGuess = static delegate { return Task.CompletedTask; }; - public event Func OnTimeout = static delegate { return Task.CompletedTask; }; - public event Func OnEnded = static delegate { return Task.CompletedTask; }; - #endregion - - private bool _isStopped; - - public TriviaQuestion? CurrentQuestion { get; set; } - - - private readonly ConcurrentDictionary _users = new (); - - private readonly Channel<(TriviaUser User, string Input)> _inputs - = Channel.CreateUnbounded<(TriviaUser, string)>(new UnboundedChannelOptions - { - AllowSynchronousContinuations = true, - SingleReader = true, - SingleWriter = false, - }); - - public TriviaGame(TriviaOptions options, ILocalDataCache cache) - { - _opts = options; - - _questionPool = _opts.IsPokemon - ? new PokemonQuestionPool(cache) - : new DefaultQuestionPool(cache); - - } - public async Task RunAsync() - { - await GameLoop(); - } - - private async Task GameLoop() - { - Task TimeOutFactory() => Task.Delay(_opts.QuestionTimer * 1000 / 2); - - var errorCount = 0; - var inactivity = 0; - - // loop until game is stopped - // each iteration is one round - var firstRun = true; - try - { - while (!_isStopped) - { - if (errorCount >= 5) - { - Log.Warning("Trivia errored 5 times and will quit"); - break; - } - - // wait for 3 seconds before posting the next question - if (firstRun) - { - firstRun = false; - } - else - { - await Task.Delay(3000); - } - - var maybeQuestion = await _questionPool.GetQuestionAsync(); - - if (maybeQuestion is not { } question) - { - // if question is null (ran out of question, or other bugg ) - stop - break; - } - - CurrentQuestion = question; - try - { - // clear out all of the past guesses - while (_inputs.Reader.TryRead(out _)) - ; - - await OnQuestion(this, question); - } - catch (Exception ex) - { - Log.Warning(ex, "Error executing OnQuestion: {Message}", ex.Message); - errorCount++; - continue; - } - - - // just keep looping through user inputs until someone guesses the answer - // or the timer expires - var halfGuessTimerTask = TimeOutFactory(); - var hintSent = false; - var guessed = false; - while (true) - { - using var readCancel = new CancellationTokenSource(); - var readTask = _inputs.Reader.ReadAsync(readCancel.Token).AsTask(); - - // wait for either someone to attempt to guess - // or for timeout - var task = await Task.WhenAny(readTask, halfGuessTimerTask); - - // if the task which completed is the timeout task - if (task == halfGuessTimerTask) - { - readCancel.Cancel(); - - // if hint is already sent, means time expired - // break (end the round) - if (hintSent) - break; - - // else, means half time passed, send a hint - hintSent = true; - // start a new countdown of the same length - halfGuessTimerTask = TimeOutFactory(); - if (!_opts.NoHint) - { - // send a hint out - await OnHint(this, question); - } - - continue; - } - - // otherwise, read task is successful, and we're gonna - // get the user input data - var (user, input) = await readTask; - - // check the guess - if (question.IsAnswerCorrect(input)) - { - // add 1 point to the user - var val = _users.AddOrUpdate(user.Id, 1, (_, points) => ++points); - guessed = true; - - // reset inactivity counter - inactivity = 0; - errorCount = 0; - - var isWin = false; - // if user won the game, tell the game to stop - if (_opts.WinRequirement != 0 && val >= _opts.WinRequirement) - { - _isStopped = true; - isWin = true; - } - - // call onguess - await OnGuess(this, user, question, isWin); - break; - } - } - - if (!guessed) - { - await OnTimeout(this, question); - - if (_opts.Timeout != 0 && ++inactivity >= _opts.Timeout) - { - Log.Information("Trivia game is stopping due to inactivity"); - break; - } - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Fatal error in trivia game: {ErrorMessage}", ex.Message); - } - finally - { - // make sure game is set as ended - _isStopped = true; - _ = OnEnded(this); - } - } - - public IReadOnlyList<(ulong User, int points)> GetLeaderboard() - => _users.Select(x => (x.Key, x.Value)).ToArray(); - - public ValueTask InputAsync(TriviaUser user, string input) - => _inputs.Writer.WriteAsync((user, input)); - - public bool Stop() - { - var isStopped = _isStopped; - _isStopped = true; - return !isStopped; - } - - public async ValueTask TriggerStatsAsync() - { - await OnStats(this); - } - - public async Task TriggerQuestionAsync() - { - if(CurrentQuestion is TriviaQuestion q) - await OnQuestion(this, q); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGamesService.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGamesService.cs deleted file mode 100644 index ba3a83f..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaGamesService.cs +++ /dev/null @@ -1,37 +0,0 @@ -#nullable disable -using Ellie.Common.ModuleBehaviors; -using Ellie.Modules.Games.Common.Trivia; - -namespace Ellie.Modules.Games; - -public sealed class TriviaGamesService : IReadyExecutor, IEService -{ - private readonly DiscordSocketClient _client; - public ConcurrentDictionary RunningTrivias { get; } = new(); - - public TriviaGamesService(DiscordSocketClient client) - { - _client = client; - } - - public Task OnReadyAsync() - { - _client.MessageReceived += OnMessageReceived; - - return Task.CompletedTask; - } - - private async Task OnMessageReceived(SocketMessage msg) - { - if (msg.Author.IsBot) - return; - - var umsg = msg as SocketUserMessage; - - if (umsg?.Channel is not IGuildChannel gc) - return; - - if (RunningTrivias.TryGetValue(gc.GuildId, out var tg)) - await tg.InputAsync(new(umsg.Author.Mention, umsg.Author.Id), umsg.Content); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaOptions.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaOptions.cs deleted file mode 100644 index 303a5ab..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -#nullable disable -using CommandLine; - -namespace Ellie.Modules.Games.Common.Trivia; - -public class TriviaOptions : IEllieCommandOptions -{ - [Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")] - public bool IsPokemon { get; set; } = false; - - [Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")] - public bool NoHint { get; set; } = false; - - [Option('w', - "win-req", - Required = false, - Default = 10, - HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")] - public int WinRequirement { get; set; } = 10; - - [Option('q', - "question-timer", - Required = false, - Default = 30, - HelpText = "How long until the question ends. Default 30.")] - public int QuestionTimer { get; set; } = 30; - - [Option('t', - "timeout", - Required = false, - Default = 10, - HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")] - public int Timeout { get; set; } = 10; - - public void NormalizeOptions() - { - if (WinRequirement < 0) - WinRequirement = 10; - if (QuestionTimer is < 10 or > 300) - QuestionTimer = 30; - if (Timeout is < 0 or > 20) - Timeout = 10; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaQuestion.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaQuestion.cs deleted file mode 100644 index c8bd521..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaQuestion.cs +++ /dev/null @@ -1,115 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace Ellie.Modules.Games.Common.Trivia; - -public class TriviaQuestion -{ - public const int MAX_STRING_LENGTH = 22; - - //represents the min size to judge levDistance with - private static readonly HashSet> _strictness = new() - { - new(9, 0), - new(14, 1), - new(19, 2), - new(22, 3) - }; - - public string Category - => _qModel.Category; - - public string Question - => _qModel.Question; - - public string ImageUrl - => _qModel.ImageUrl; - - public string AnswerImageUrl - => _qModel.AnswerImageUrl ?? ImageUrl; - - public string Answer - => _qModel.Answer; - - public string CleanAnswer - => cleanAnswer ?? (cleanAnswer = Clean(Answer)); - - private string cleanAnswer; - private readonly TriviaQuestionModel _qModel; - - public TriviaQuestion(TriviaQuestionModel qModel) - { - _qModel = qModel; - } - - public string GetHint() - => Scramble(Answer); - - public bool IsAnswerCorrect(string guess) - { - if (Answer.Equals(guess, StringComparison.InvariantCulture)) - return true; - var cleanGuess = Clean(guess); - if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture)) - return true; - - var levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess); - var levDistanceNormal = Answer.LevenshteinDistance(guess); - return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean) - || JudgeGuess(Answer.Length, guess.Length, levDistanceNormal); - } - - private static bool JudgeGuess(int guessLength, int answerLength, int levDistance) - { - foreach (var level in _strictness) - { - if (guessLength <= level.Item1 || answerLength <= level.Item1) - { - if (levDistance <= level.Item2) - return true; - return false; - } - } - - return false; - } - - private static string Clean(string str) - { - str = " " + str.ToLowerInvariant() + " "; - str = Regex.Replace(str, @"\s+", " "); - str = Regex.Replace(str, @"[^\w\d\s]", ""); - //Here's where custom modification can be done - str = Regex.Replace(str, @"\s(a|an|the|of|in|for|to|as|at|be)\s", " "); - //End custom mod and cleanup whitespace - str = Regex.Replace(str, @"^\s+", ""); - str = Regex.Replace(str, @"\s+$", ""); - //Trim the really long answers - str = str.Length <= MAX_STRING_LENGTH ? str : str[..MAX_STRING_LENGTH]; - return str; - } - - private static string Scramble(string word) - { - var letters = word.ToCharArray(); - var count = 0; - for (var i = 0; i < letters.Length; i++) - { - if (letters[i] == ' ') - continue; - - count++; - if (count <= letters.Length / 5) - continue; - - if (count % 3 == 0) - continue; - - if (letters[i] != ' ') - letters[i] = '_'; - } - - return string.Join(" ", - new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable()); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaUser.cs b/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaUser.cs deleted file mode 100644 index f2e9b80..0000000 --- a/src/Ellie.Bot.Modules.Gambling/Games/Trivia/TriviaUser.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Ellie.Modules.Games.Common.Trivia; - -public record class TriviaUser(string Name, ulong Id); \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Help/CommandJsonObject.cs b/src/Ellie.Bot.Modules.Help/CommandJsonObject.cs deleted file mode 100644 index 6bbbd66..0000000 --- a/src/Ellie.Bot.Modules.Help/CommandJsonObject.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Help; - -internal class CommandJsonObject -{ - public string[] Aliases { get; set; } - public string Description { get; set; } - public string[] Usage { get; set; } - public string Submodule { get; set; } - public string Module { get; set; } - public List Options { get; set; } - public string[] Requirements { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Help/CommandsOptions.cs b/src/Ellie.Bot.Modules.Help/CommandsOptions.cs deleted file mode 100644 index 9189154..0000000 --- a/src/Ellie.Bot.Modules.Help/CommandsOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable disable -using CommandLine; - -namespace Ellie.Modules.Help.Common; - -public class CommandsOptions : IEllieCommandOptions -{ - public enum ViewType - { - Hide, - Cross, - All - } - - [Option('v', - "view", - Required = false, - Default = ViewType.Hide, - HelpText = - "Specifies how to output the list of commands. 0 - Hide commands which you can't use, 1 - Cross out commands which you can't use, 2 - Show all.")] - public ViewType View { get; set; } = ViewType.Hide; - - public void NormalizeOptions() - { - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Help/Help.cs b/src/Ellie.Bot.Modules.Help/Help.cs deleted file mode 100644 index 853fb3c..0000000 --- a/src/Ellie.Bot.Modules.Help/Help.cs +++ /dev/null @@ -1,588 +0,0 @@ -#nullable disable -using Amazon.S3; -using Ellie.Marmalade; -using Ellie.Modules.Help.Common; -using Ellie.Modules.Help.Services; -using Newtonsoft.Json; -using System.Text; -using System.Text.Json; -using Ellie.Bot.Common; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace Ellie.Modules.Help; - -public sealed class Help : EllieModule -{ - public const string PATREON_URL = "https://patreon.com/emotionchild"; - public const string PAYPAL_URL = "https://paypal.me/EmotionChild"; - - private readonly ICommandsUtilityService _cus; - private readonly CommandService _cmds; - private readonly BotConfigService _bss; - private readonly IPermissionChecker _perms; - private readonly IServiceProvider _services; - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; - - private readonly AsyncLazy _lazyClientId; - private readonly IMarmaladeLoaderSevice _marmalades; - - public Help( - ICommandsUtilityService _cus, - IPermissionChecker perms, - CommandService cmds, - BotConfigService bss, - IServiceProvider services, - DiscordSocketClient client, - IBotStrings strings, - IMarmaladeLoaderSevice marmalades) - { - this._cus = _cus; - _cmds = cmds; - _bss = bss; - _perms = perms; - _services = services; - _client = client; - _strings = strings; - _marmalades = marmalades; - - _lazyClientId = new(async () => (await _client.GetApplicationInfoAsync()).Id); - } - - public async Task GetHelpString() - { - var botSettings = _bss.Data; - if (string.IsNullOrWhiteSpace(botSettings.HelpText) || botSettings.HelpText == "-") - return default; - - var clientId = await _lazyClientId.Value; - var r = new ReplacementBuilder().WithDefault(Context) - .WithOverride("{0}", () => clientId.ToString()) - .WithOverride("{1}", () => prefix) - .WithOverride("%prefix%", () => prefix) - .WithOverride("%bot.prefix%", () => prefix) - .Build(); - - var text = SmartText.CreateFrom(botSettings.HelpText); - return r.Replace(text); - } - - [Cmd] - public async Task Modules(int page = 1) - { - if (--page < 0) - return; - - var topLevelModules = new List(); - foreach (var m in _cmds.Modules.GroupBy(x => x.GetTopLevelModule()).Select(x => x.Key)) - { - var result = await _perms.CheckAsync(ctx.Guild, ctx.Channel, ctx.User, - m.Name, null); - - if (result.IsT0) - topLevelModules.Add(m); - } - - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.list_of_modules)); - - var localModules = topLevelModules.Skip(12 * cur).Take(12).ToList(); - - if (!localModules.Any()) - { - embed = embed.WithOkColor().WithDescription(GetText(strs.module_page_empty)); - return embed; - } - - localModules.OrderBy(module => module.Name) - .ToList() - .ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}", - GetModuleDescription(module.Name) - + "\n" - + Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))), - true)); - - return embed; - }, - topLevelModules.Count(), - 12, - false); - } - - private string GetModuleDescription(string moduleName) - { - var key = GetModuleLocStr(moduleName); - - if (key.Key == strs.module_description_missing.Key) - { - var desc = _marmalades - .GetLoadedMarmalades(Culture) - .FirstOrDefault(m => m.Canaries - .Any(x => x.Name.Equals(moduleName, - StringComparison.InvariantCultureIgnoreCase))) - ?.Description; - - if (desc is not null) - return desc; - } - - return GetText(key); - } - - private LocStr GetModuleLocStr(string moduleName) - { - switch (moduleName.ToLowerInvariant()) - { - case "help": - return strs.module_description_help; - case "administration": - return strs.module_description_administration; - case "expressions": - return strs.module_description_expressions; - case "searches": - return strs.module_description_searches; - case "utility": - return strs.module_description_utility; - case "games": - return strs.module_description_games; - case "gambling": - return strs.module_description_gambling; - case "music": - return strs.module_description_music; - case "nsfw": - return strs.module_description_nsfw; - case "permissions": - return strs.module_description_permissions; - case "xp": - return strs.module_description_xp; - case "marmalade": - return strs.module_description_marmalade; - case "patronage": - return strs.module_description_patronage; - default: - return strs.module_description_missing; - } - } - - private string GetModuleEmoji(string moduleName) - { - moduleName = moduleName.ToLowerInvariant(); - switch (moduleName) - { - case "help": - return "❓"; - case "administration": - return "🛠️"; - case "expressions": - return "🗣️"; - case "searches": - return "🔍"; - case "utility": - return "🔧"; - case "games": - return "🎲"; - case "gambling": - return "💰"; - case "music": - return "🎶"; - case "nsfw": - return "😳"; - case "permissions": - return "🚓"; - case "xp": - return "📝"; - case "patronage": - return "💝"; - default: - return "📖"; - } - } - - [Cmd] - [EllieOptions] - public async Task Commands(string module = null, params string[] args) - { - if (string.IsNullOrWhiteSpace(module)) - { - await Modules(); - return; - } - - var (opts, _) = OptionsParser.ParseFrom(new CommandsOptions(), args); - - // Find commands for that module - // don't show commands which are blocked - // order by name - var allowed = new List(); - - foreach (var cmd in _cmds.Commands - .Where(c => c.Module.GetTopLevelModule() - .Name - .StartsWith(module, StringComparison.InvariantCultureIgnoreCase))) - { - var result = await _perms.CheckAsync(ctx.Guild, ctx.Channel, ctx.User, cmd.Module.GetTopLevelModule().Name, - cmd.Name); - if (result.IsT0) - allowed.Add(cmd); - } - - var cmds = allowed.OrderBy(c => c.Aliases[0]) - .DistinctBy(x => x.Aliases[0]) - .ToList(); - - - // check preconditions for all commands, but only if it's not 'all' - // because all will show all commands anyway, no need to check - var succ = new HashSet(); - if (opts.View != CommandsOptions.ViewType.All) - { - succ = new((await cmds.Select(async x => - { - var pre = await x.CheckPreconditionsAsync(Context, _services); - return (Cmd: x, Succ: pre.IsSuccess); - }) - .WhenAll()).Where(x => x.Succ) - .Select(x => x.Cmd)); - - if (opts.View == CommandsOptions.ViewType.Hide) - // if hidden is specified, completely remove these commands from the list - cmds = cmds.Where(x => succ.Contains(x)).ToList(); - } - - var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName()) - .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count()) - .ToList(); - - if (cmdsWithGroup.Count == 0) - { - if (opts.View != CommandsOptions.ViewType.Hide) - await ReplyErrorLocalizedAsync(strs.module_not_found); - else - await ReplyErrorLocalizedAsync(strs.module_not_found_or_cant_exec); - return; - } - - var cnt = 0; - var groups = cmdsWithGroup.GroupBy(_ => cnt++ / 48).ToArray(); - var embed = _eb.Create().WithOkColor(); - foreach (var g in groups) - { - var last = g.Count(); - for (var i = 0; i < last; i++) - { - var transformed = g.ElementAt(i) - .Select(x => - { - //if cross is specified, and the command doesn't satisfy the requirements, cross it out - if (opts.View == CommandsOptions.ViewType.Cross) - { - return - $"{(succ.Contains(x) ? "✅" : "❌")}{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; - } - - return - $"{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; - }); - - if (i == last - 1 && (i + 1) % 2 != 0) - { - transformed = transformed.Chunk(2) - .Select(x => - { - if (x.Count() == 1) - return $"{x.First()}"; - return string.Concat(x); - }); - } - - embed.AddField(g.ElementAt(i).Key, "```css\n" + string.Join("\n", transformed) + "\n```", true); - } - } - - embed.WithFooter(GetText(strs.commands_instr(prefix))); - await ctx.Channel.EmbedAsync(embed); - } - - private async Task Group(ModuleInfo group) - { - var eb = _eb.Create(ctx) - .WithTitle(GetText(strs.cmd_group_commands(group.Name))) - .WithOkColor(); - - foreach (var cmd in group.Commands) - { - eb.AddField(prefix + cmd.Aliases.First(), cmd.RealSummary(_strings, _marmalades, Culture, prefix)); - } - - await ctx.Channel.EmbedAsync(eb); - } - - [Cmd] - [Priority(0)] - public async Task H([Leftover] string fail) - { - var prefixless = - _cmds.Commands.FirstOrDefault(x => x.Aliases.Any(cmdName => cmdName.ToLowerInvariant() == fail)); - if (prefixless is not null) - { - await H(prefixless); - return; - } - - if (fail.StartsWith(prefix)) - fail = fail.Substring(prefix.Length); - - var group = _cmds.Modules - .SelectMany(x => x.Submodules) - .Where(x => !string.IsNullOrWhiteSpace(x.Group)) - .FirstOrDefault(x => x.Group.Equals(fail, StringComparison.InvariantCultureIgnoreCase)); - - if (group is not null) - { - await Group(group); - return; - } - - await ReplyErrorLocalizedAsync(strs.command_not_found); - } - - [Cmd] - [Priority(1)] - public async Task H([Leftover] CommandInfo com = null) - { - var channel = ctx.Channel; - - if (com is null) - { - var ch = channel is ITextChannel ? await ctx.User.CreateDMChannelAsync() : channel; - try - { - var data = await GetHelpString(); - if (data == default) - return; - await ch.SendAsync(data); - try - { - await ctx.OkAsync(); - } - catch - { - } // ignore if bot can't react - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.cant_dm); - } - - return; - } - - var embed = _cus.GetCommandHelp(com, ctx.Guild); - await channel.EmbedAsync(embed); - } - - [Cmd] - [OwnerOnly] - public async Task GenCmdList() - { - _ = ctx.Channel.TriggerTypingAsync(); - - // order commands by top level module name - // and make a dictionary of > - var cmdData = _cmds.Commands.GroupBy(x => x.Module.GetTopLevelModule().Name) - .OrderBy(x => x.Key) - .ToDictionary(x => x.Key, - x => x.DistinctBy(c => c.Aliases.First()) - .Select(com => - { - List optHelpStr = null; - - var opt = CommandsUtilityService.GetEllieOptionType(com.Attributes); - if (opt is not null) - optHelpStr = CommandsUtilityService.GetCommandOptionHelpList(opt); - - return new CommandJsonObject - { - Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(), - Description = com.RealSummary(_strings, _marmalades, Culture, prefix), - Usage = com.RealRemarksArr(_strings, _marmalades, Culture, prefix), - Submodule = com.Module.Name, - Module = com.Module.GetTopLevelModule().Name, - Options = optHelpStr, - Requirements = CommandsUtilityService.GetCommandRequirements(com) - }; - }) - .ToList()); - - var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented); - var uploadData = JsonConvert.SerializeObject(cmdData, Formatting.None); - - // for example https://nyc.digitaloceanspaces.com (without your space name) - var serviceUrl = Environment.GetEnvironmentVariable("do_spaces_address"); - - // generate spaces access key on https://cloud.digitalocean.com/account/api/tokens - // you will get 2 keys, first, shorter one is id, longer one is secret - var accessKey = Environment.GetEnvironmentVariable("do_access_key_id"); - var secretAcccessKey = Environment.GetEnvironmentVariable("do_access_key_secret"); - - // if all env vars are set, upload the unindented file (to save space) there - if (!(serviceUrl is null || accessKey is null || secretAcccessKey is null)) - { - var config = new AmazonS3Config - { - ServiceURL = serviceUrl - }; - - using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config); - - using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config)) - { - await client.PutObjectAsync(new() - { - BucketName = "ellie", - ContentType = "application/json", - ContentBody = uploadData, - // either use a path provided in the argument or the default one for public ellie, other/cmds.json - Key = $"cmds/{StatsService.BOT_VERSION}.json", - CannedACL = S3CannedACL.PublicRead - }); - } - - - var versionListString = "[]"; - try - { - using var oldVersionObject = await dlClient.GetObjectAsync(new() - { - BucketName = "ellie", - Key = "cmds/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)) - { - // save the file with new version added - // versionList.Add(StatsService.BotVersion); - versionListString = JsonSerializer.Serialize(versionList.Prepend(StatsService.BOT_VERSION), - new JsonSerializerOptions - { - WriteIndented = true - }); - - // upload the updated version list - using var client = new AmazonS3Client(accessKey, secretAcccessKey, config); - await client.PutObjectAsync(new() - { - BucketName = "ellie", - ContentType = "application/json", - ContentBody = versionListString, - // either use a path provided in the argument or the default one for public ellie, other/cmds.json - Key = "cmds/versions.json", - CannedACL = S3CannedACL.PublicRead - }); - } - else - { - Log.Warning( - "Version {Version} already exists in the version file. " + "Did you forget to increment it?", - StatsService.BOT_VERSION); - } - } - - // also send the file, but indented one, to chat - await using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData)); - await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen)); - } - - [Cmd] - public async Task Guide() - => await ConfirmLocalizedAsync(strs.guide("https://commands.elliebot.net", - "https://docs.elliebot.net/")); - - - private Task SelfhostAction(SocketMessageComponent smc, object _) - => smc.RespondConfirmAsync(_eb, - """ - - In case you don't want or cannot Donate to Ellie project, but you - - Ellie is a completely free and fully [open source](https://toastielab.dev/EllieBotDevs/Ellie-bot) 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/emotionchild) to keep the development going :) - """, - true); - - [Cmd] - [OnlyPublicBot] - public async Task Donate() - { - // => new EllieInteractionData(new Emoji("🖥️"), "donate:selfhosting", "Selfhosting"); - var selfhostInter = _inter.Create(ctx.User.Id, - new SimpleInteraction(new ButtonBuilder( - emote: new Emoji("🖥️"), - customId: "donate:selfhosting", - label: "Selfhosting"), - SelfhostAction)); - - var eb = _eb.Create(ctx) - .WithOkColor() - .WithTitle("Thank you for considering to donate to the Ellie project!"); - - eb - .WithDescription("Ellie 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/emotionchild)") - .AddField("Donation Instructions", - $@" -🗒️ Before pledging it is recommended to open your DMs as Ellie 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) ⏰ - -Ellie 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 ellie support server and ask for help in the #help channel - """); - - try - { - await (await ctx.User.CreateDMChannelAsync()).EmbedAsync(eb, inter: selfhostInter); - _ = ctx.OkAsync(); - } - catch - { - await ReplyErrorLocalizedAsync(strs.cant_dm); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Help/HelpService.cs b/src/Ellie.Bot.Modules.Help/HelpService.cs deleted file mode 100644 index c2d3ad9..0000000 --- a/src/Ellie.Bot.Modules.Help/HelpService.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable disable -using Ellie.Common.ModuleBehaviors; - -namespace Ellie.Modules.Help.Services; - -public class HelpService : IExecNoCommand, IEService -{ - private readonly BotConfigService _bss; - - public HelpService(BotConfigService bss) - { - _bss = bss; - } - - public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - { - var settings = _bss.Data; - if (guild is null) - { - if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-") - return Task.CompletedTask; - - // only send dm help text if it contains one of the keywords, if they're specified - // if they're not, then reply to every DM - if (settings.DmHelpTextKeywords is not null && - !settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k))) - return Task.CompletedTask; - - var rep = new ReplacementBuilder().WithOverride("%prefix%", () => _bss.Data.Prefix) - .WithOverride("%bot.prefix%", () => _bss.Data.Prefix) - .WithUser(msg.Author) - .Build(); - - var text = SmartText.CreateFrom(settings.DmHelpText); - text = rep.Replace(text); - - return msg.Channel.SendAsync(text); - } - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/CleanupCommands.cs b/src/Ellie.Bot.Modules.Music/CleanupCommands.cs deleted file mode 100644 index b5f85c6..0000000 --- a/src/Ellie.Bot.Modules.Music/CleanupCommands.cs +++ /dev/null @@ -1,24 +0,0 @@ -using LinqToDB; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Music; - -public sealed partial class Music -{ - public class CleanupCommands : EllieModule - { - private readonly DbService _db; - - public CleanupCommands(DbService db) - { - _db = db; - } - - public async Task DeletePlaylists() - { - await using var uow = _db.GetDbContext(); - await uow.Set().DeleteAsync(); - await uow.SaveChangesAsync(); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Music.cs b/src/Ellie.Bot.Modules.Music/Music.cs deleted file mode 100644 index 8224bbf..0000000 --- a/src/Ellie.Bot.Modules.Music/Music.cs +++ /dev/null @@ -1,758 +0,0 @@ -#nullable disable -using Ellie.Modules.Music.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Music; - -[NoPublicBot] -public sealed partial class Music : EllieModule -{ - public enum All { All = -1 } - - public enum InputRepeatType - { - N = 0, No = 0, None = 0, - T = 1, Track = 1, S = 1, Song = 1, - Q = 2, Queue = 2, Playlist = 2, Pl = 2 - } - - public const string MUSIC_ICON_URL = "https://i.imgur.com/nhKS3PT.png"; - - private const int LQ_ITEMS_PER_PAGE = 9; - - private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1); - private readonly ILogCommandService _logService; - - public Music(ILogCommandService logService) - => _logService = logService; - - private async Task ValidateAsync() - { - var user = (IGuildUser)ctx.User; - var userVoiceChannelId = user.VoiceChannel?.Id; - - if (userVoiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return false; - } - - var currentUser = await ctx.Guild.GetCurrentUserAsync(); - if (currentUser.VoiceChannel?.Id != userVoiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return false; - } - - return true; - } - - private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) - { - botUser ??= await ctx.Guild.GetCurrentUserAsync(); - await _voiceChannelLock.WaitAsync(); - try - { - if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) - await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); - } - finally - { - _voiceChannelLock.Release(); - } - } - - private async Task QueuePreconditionInternalAsync() - { - var user = (IGuildUser)ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return false; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return false; - } - - return true; - } - - private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null) - { - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, ctx.User.ToString(), asNext, forcePlatform); - if (trackInfo is null) - { - await ReplyErrorLocalizedAsync(strs.track_not_found); - return; - } - - try - { - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL) - .WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ") - .WithFooter(trackInfo.Platform.ToString()); - - if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail)) - embed.WithThumbnailUrl(trackInfo.Thumbnail); - - var queuedMessage = await _service.SendToOutputAsync(ctx.Guild.Id, embed); - queuedMessage?.DeleteAfter(10, _logService); - if (mp.IsStopped) - { - var msg = await ReplyPendingLocalizedAsync(strs.queue_stopped(Format.Code(prefix + "play"))); - msg.DeleteAfter(10, _logService); - } - } - catch - { - // ignored - } - } - - private async Task MoveToIndex(int index) - { - if (--index < 0) - return; - - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.MoveTo(index); - } - - // join vc - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Join() - { - var user = (IGuildUser)ctx.User; - - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value); - } - - // leave vc (destroy) - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Destroy() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.LeaveVoiceChannelAsync(ctx.Guild.Id); - } - - // play - no args = next - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(2)] - public Task Play() - => Next(); - - // play - index = skip to that index - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Play(int index) - => MoveToIndex(index); - - // play - query = q(query) - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task Play([Leftover] string query) - => QueueByQuery(query); - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Queue([Leftover] string query) - => QueueByQuery(query); - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task QueueNext([Leftover] string query) - => QueueByQuery(query, true); - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Volume(int vol) - { - if (vol is < 0 or > 100) - { - await ReplyErrorLocalizedAsync(strs.volume_input_invalid); - return; - } - - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.SetVolumeAsync(ctx.Guild.Id, vol); - await ReplyConfirmLocalizedAsync(strs.volume_set(vol)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Next() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id); - if (!success) - await ReplyErrorLocalizedAsync(strs.no_player); - } - - // list queue, relevant page - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ListQueue() - { - // show page with the current track - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - await ListQueue((mp.CurrentIndex / LQ_ITEMS_PER_PAGE) + 1); - } - - // list queue, specify page - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ListQueue(int page) - { - if (--page < 0) - return; - - IReadOnlyCollection tracks; - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - IEmbedBuilder PrintAction(int curPage) - { - var desc = string.Empty; - var current = mp.GetCurrentTrack(out var currentIndex); - if (current is not null) - desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc; - - var repeatType = mp.Repeat; - var add = string.Empty; - if (mp.IsStopped) - add += Format.Bold(GetText(strs.queue_stopped(Format.Code(prefix + "play")))) + "\n"; - // var mps = mp.MaxPlaytimeSeconds; - // if (mps > 0) - // add += Format.Bold(GetText(strs.song_skips_after(TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss")))) + "\n"; - if (repeatType == PlayerRepeatType.Track) - add += "🔂 " + GetText(strs.repeating_track) + "\n"; - else - { - if (mp.AutoPlay) - add += "↪ " + GetText(strs.autoplaying) + "\n"; - // if (mp.FairPlay && !mp.Autoplay) - // add += " " + GetText(strs.fairplay) + "\n"; - if (repeatType == PlayerRepeatType.Queue) - add += "🔁 " + GetText(strs.repeating_queue) + "\n"; - } - - - desc += tracks.Skip(LQ_ITEMS_PER_PAGE * curPage) - .Take(LQ_ITEMS_PER_PAGE) - .Select((v, index) => - { - index += LQ_ITEMS_PER_PAGE * curPage; - if (index == currentIndex) - return $"**⇒**`{index + 1}.` {v.PrettyFullName()}"; - - return $"`{index + 1}.` {v.PrettyFullName()}"; - }) - .Join('\n'); - - if (!string.IsNullOrWhiteSpace(add)) - desc = add + "\n" + desc; - - var embed = _eb.Create() - .WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)), - MUSIC_ICON_URL) - .WithDescription(desc) - .WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") - .WithOkColor(); - - return embed; - } - - await ctx.SendPaginatedConfirmAsync(page, PrintAction, tracks.Count, LQ_ITEMS_PER_PAGE, false); - } - - // search - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task QueueSearch([Leftover] string query) - { - _ = ctx.Channel.TriggerTypingAsync(); - - var videos = await _service.SearchVideosAsync(query); - - if (videos.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.track_not_found); - return; - } - - var resultsString = videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}").Join('\n'); - - var msg = await SendConfirmAsync(resultsString); - - try - { - var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id); - if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count) - { - _logService.AddDeleteIgnore(msg.Id); - try - { - await msg.DeleteAsync(); - } - catch - { - } - - return; - } - - query = videos[index].Url; - - await Play(query); - } - finally - { - _logService.AddDeleteIgnore(msg.Id); - try - { - await msg.DeleteAsync(); - } - catch - { - } - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task TrackRemove(int index) - { - if (index < 1) - { - await ReplyErrorLocalizedAsync(strs.removed_track_error); - return; - } - - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - if (!mp.TryRemoveTrackAt(index - 1, out var track)) - { - await ReplyErrorLocalizedAsync(strs.removed_track_error); - return; - } - - var embed = _eb.Create() - .WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL) - .WithDescription(track.PrettyName()) - .WithFooter(track.PrettyInfo()) - .WithErrorColor(); - - await _service.SendToOutputAsync(ctx.Guild.Id, embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task TrackRemove(All _ = All.All) - { - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.Clear(); - await ReplyConfirmLocalizedAsync(strs.queue_cleared); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Stop() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.Stop(); - } - - private PlayerRepeatType InputToDbType(InputRepeatType type) - => type switch - { - InputRepeatType.None => PlayerRepeatType.None, - InputRepeatType.Queue => PlayerRepeatType.Queue, - InputRepeatType.Track => PlayerRepeatType.Track, - _ => PlayerRepeatType.Queue - }; - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue) - { - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type)); - - if (type == InputRepeatType.None) - await ReplyConfirmLocalizedAsync(strs.repeating_none); - else if (type == InputRepeatType.Queue) - await ReplyConfirmLocalizedAsync(strs.repeating_queue); - else - await ReplyConfirmLocalizedAsync(strs.repeating_track); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Pause() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.TogglePause(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task Radio(string radioLink) - => QueueByQuery(radioLink, false, MusicPlatform.Radio); - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public Task Local([Leftover] string path) - => QueueByQuery(path, false, MusicPlatform.Local); - - [Cmd] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task LocalPlaylist([Leftover] string dirPath) - { - if (string.IsNullOrWhiteSpace(dirPath)) - return; - - var user = (IGuildUser)ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return; - } - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString()); - - await ReplyConfirmLocalizedAsync(strs.dir_queue_complete); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task TrackMove(int from, int to) - { - if (--from < 0 || --to < 0 || from == to) - { - await ReplyErrorLocalizedAsync(strs.invalid_input); - return; - } - - var valid = await ValidateAsync(); - if (!valid) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - var track = mp.MoveTrack(from, to); - if (track is null) - { - await ReplyErrorLocalizedAsync(strs.invalid_input); - return; - } - - var embed = _eb.Create() - .WithTitle(track.Title.TrimTo(65)) - .WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL) - .AddField(GetText(strs.from_position), $"#{from + 1}", true) - .AddField(GetText(strs.to_position), $"#{to + 1}", true) - .WithOkColor(); - - if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) - embed.WithUrl(track.Url); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public Task SoundCloudQueue([Leftover] string query) - => QueueByQuery(query, false, MusicPlatform.SoundCloud); - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudPl([Leftover] string playlist) - { - if (string.IsNullOrWhiteSpace(playlist)) - return; - - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString()); - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Playlist([Leftover] string playlistQuery) - { - if (string.IsNullOrWhiteSpace(playlistQuery)) - return; - - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - - var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString()); - if (queuedCount == 0) - { - await ReplyErrorLocalizedAsync(strs.no_search_results); - return; - } - - await ctx.OkAsync(); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task NowPlaying() - { - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - var currentTrack = mp.GetCurrentTrack(out _); - if (currentTrack is null) - return; - - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL) - .WithDescription(currentTrack.PrettyName()) - .WithThumbnailUrl(currentTrack.Thumbnail) - .WithFooter( - $"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task PlaylistShuffle() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.ShuffleQueue(); - await ReplyConfirmLocalizedAsync(strs.queue_shuffled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task SetMusicChannel() - { - await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id); - - await ReplyConfirmLocalizedAsync(strs.set_music_channel); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task UnsetMusicChannel() - { - await _service.SetMusicChannelAsync(ctx.Guild.Id, null); - - await ReplyConfirmLocalizedAsync(strs.unset_music_channel); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AutoDisconnect() - { - var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id); - - if (newState) - await ReplyConfirmLocalizedAsync(strs.autodc_enable); - else - await ReplyConfirmLocalizedAsync(strs.autodc_disable); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task MusicQuality() - { - var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task MusicQuality(QualityPreset preset) - { - await _service.SetMusicQualityAsync(ctx.Guild.Id, preset); - await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task QueueAutoPlay() - { - var newValue = await _service.ToggleQueueAutoPlayAsync(ctx.Guild.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.music_autoplay_on); - else - await ReplyConfirmLocalizedAsync(strs.music_autoplay_off); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/PlaylistCommands.cs b/src/Ellie.Bot.Modules.Music/PlaylistCommands.cs deleted file mode 100644 index 293ab3d..0000000 --- a/src/Ellie.Bot.Modules.Music/PlaylistCommands.cs +++ /dev/null @@ -1,229 +0,0 @@ -#nullable disable -using Ellie.Db; -using Ellie.Modules.Music.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Music; - -public sealed partial class Music -{ - [Group] - public sealed partial class PlaylistCommands : EllieModule - { - private static readonly SemaphoreSlim _playlistLock = new(1, 1); - private readonly DbService _db; - private readonly IBotCredentials _creds; - - public PlaylistCommands(DbService db, IBotCredentials creds) - { - _db = db; - _creds = creds; - } - - private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) - { - botUser ??= await ctx.Guild.GetCurrentUserAsync(); - await _voiceChannelLock.WaitAsync(); - try - { - if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) - await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); - } - finally - { - _voiceChannelLock.Release(); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Playlists([Leftover] int num = 1) - { - if (num <= 0) - return; - - List playlists; - - await using (var uow = _db.GetDbContext()) - { - playlists = uow.Set().GetPlaylistsOnPage(num); - } - - var embed = _eb.Create(ctx) - .WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL) - .WithDescription(string.Join("\n", - playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count))))) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task DeletePlaylist([Leftover] int id) - { - var success = false; - try - { - await using var uow = _db.GetDbContext(); - var pl = uow.Set().FirstOrDefault(x => x.Id == id); - - if (pl is not null) - { - if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id) - { - uow.Set().Remove(pl); - await uow.SaveChangesAsync(); - success = true; - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error deleting playlist"); - } - - if (!success) - await ReplyErrorLocalizedAsync(strs.playlist_delete_fail); - else - await ReplyConfirmLocalizedAsync(strs.playlist_deleted); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task PlaylistShow(int id, int page = 1) - { - if (page-- < 1) - return; - - MusicPlaylist mpl; - await using (var uow = _db.GetDbContext()) - { - mpl = uow.Set().GetWithSongs(id); - } - - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var i = 0; - var str = string.Join("\n", - mpl.Songs.Skip(cur * 20) - .Take(20) - .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`")); - return _eb.Create().WithTitle($"\"{mpl.Name}\" by {mpl.Author}").WithOkColor().WithDescription(str); - }, - mpl.Songs.Count, - 20); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Save([Leftover] string name) - { - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - var songs = mp.GetQueuedTracks() - .Select(s => new PlaylistSong - { - Provider = s.Platform.ToString(), - ProviderType = (MusicType)s.Platform, - Title = s.Title, - Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url - }) - .ToList(); - - MusicPlaylist playlist; - await using (var uow = _db.GetDbContext()) - { - playlist = new() - { - Name = name, - Author = ctx.User.Username, - AuthorId = ctx.User.Id, - Songs = songs.ToList() - }; - uow.Set().Add(playlist); - await uow.SaveChangesAsync(); - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.playlist_saved)) - .AddField(GetText(strs.name), name) - .AddField(GetText(strs.id), playlist.Id.ToString())); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Load([Leftover] int id) - { - // expensive action, 1 at a time - await _playlistLock.WaitAsync(); - try - { - var user = (IGuildUser)ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return; - } - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - MusicPlaylist mpl; - await using (var uow = _db.GetDbContext()) - { - mpl = uow.Set().GetWithSongs(id); - } - - if (mpl is null) - { - await ReplyErrorLocalizedAsync(strs.playlist_id_not_found); - return; - } - - IUserMessage msg = null; - try - { - msg = await ctx.Channel.SendMessageAsync( - GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString())))); - } - catch (Exception) - { - } - - await mp.EnqueueManyAsync(mpl.Songs.Select(x => (x.Query, (MusicPlatform)x.ProviderType)), - ctx.User.ToString()); - - if (msg is not null) - await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete)); - } - finally - { - _playlistLock.Release(); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/AyuVoiceStateService.cs b/src/Ellie.Bot.Modules.Music/Services/AyuVoiceStateService.cs deleted file mode 100644 index 04c2b42..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/AyuVoiceStateService.cs +++ /dev/null @@ -1,218 +0,0 @@ -#nullable disable -using Ayu.Discord.Voice; -using System.Reflection; - -namespace Ellie.Modules.Music.Services; - -public class AyuVoiceStateService : IEService -{ - // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy); - // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; }; - - private readonly ConcurrentDictionary _voiceProxies = new(); - private readonly ConcurrentDictionary _voiceGatewayLocks = new(); - - private readonly DiscordSocketClient _client; - private readonly MethodInfo _sendVoiceStateUpdateMethodInfo; - private readonly object _dnetApiClient; - private readonly ulong _currentUserId; - - public AyuVoiceStateService(DiscordSocketClient client) - { - _client = client; - _currentUserId = _client.CurrentUser.Id; - - var prop = _client.GetType() - .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) - .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient"); - _dnetApiClient = prop.GetValue(_client, null); - _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType() - .GetMethod("SendVoiceStateUpdateAsync", - new[] - { - typeof(ulong), typeof(ulong?), typeof(bool), - typeof(bool), typeof(RequestOptions) - }); - - _client.LeftGuild += ClientOnLeftGuild; - } - - private Task ClientOnLeftGuild(SocketGuild guild) - { - if (_voiceProxies.TryRemove(guild.Id, out var proxy)) - { - proxy.StopGateway(); - proxy.SetGateway(null); - } - - return Task.CompletedTask; - } - - private Task InvokeSendVoiceStateUpdateAsync( - ulong guildId, - ulong? channelId = null, - bool isDeafened = false, - bool isMuted = false) - // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted); - => (Task)_sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, - new object[] { guildId, channelId, isMuted, isDeafened, null }); - - private Task SendLeaveVoiceChannelInternalAsync(ulong guildId) - => InvokeSendVoiceStateUpdateAsync(guildId); - - private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId) - => InvokeSendVoiceStateUpdateAsync(guildId, channelId); - - private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) - => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1)); - - private async Task LeaveVoiceChannelInternalAsync(ulong guildId) - { - var complete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) - { - if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId && newState.VoiceChannel?.Id is null) - complete.TrySetResult(true); - - return Task.CompletedTask; - } - - try - { - _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; - - if (_voiceProxies.TryGetValue(guildId, out var proxy)) - { - _ = proxy.StopGateway(); - proxy.SetGateway(null); - } - - await SendLeaveVoiceChannelInternalAsync(guildId); - await Task.WhenAny(Task.Delay(1500), complete.Task); - } - finally - { - _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; - } - } - - public async Task LeaveVoiceChannel(ulong guildId) - { - var gwLock = GetVoiceGatewayLock(guildId); - await gwLock.WaitAsync(); - try - { - await LeaveVoiceChannelInternalAsync(guildId); - } - finally - { - gwLock.Release(); - } - } - - private async Task InternalConnectToVcAsync(ulong guildId, ulong channelId) - { - var voiceStateUpdatedSource = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var voiceServerUpdatedSource = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) - { - if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId) - { - if (newState.VoiceChannel?.Id == channelId) - voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId); - - voiceStateUpdatedSource.TrySetResult(null); - } - - return Task.CompletedTask; - } - - Task OnVoiceServerUpdated(SocketVoiceServer data) - { - if (data.Guild.Id == guildId) - voiceServerUpdatedSource.TrySetResult(data); - - return Task.CompletedTask; - } - - try - { - _client.VoiceServerUpdated += OnVoiceServerUpdated; - _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; - - await SendJoinVoiceChannelInternalAsync(guildId, channelId); - - // create a delay task, how much to wait for gateway response - using var cts = new CancellationTokenSource(); - var delayTask = Task.Delay(2500, cts.Token); - - // either delay or successful voiceStateUpdate - var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task); - // either delay or successful voiceServerUpdate - var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task); - - // wait for both to end (max 1s) and check if either of them is a delay task - var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask); - if (results[0] == delayTask || results[1] == delayTask) - // if either is delay, return null - connection unsuccessful - return null; - else - cts.Cancel(); - - // if both are succesful, that means we can safely get - // the values from completion sources - - var session = await voiceStateUpdatedSource.Task; - - // session can be null. Means we disconnected, or connected to the wrong channel (?!) - if (session is null) - return null; - - var voiceServerData = await voiceServerUpdatedSource.Task; - - VoiceGateway CreateVoiceGatewayLocal() - { - return new(guildId, _currentUserId, session, voiceServerData.Token, voiceServerData.Endpoint); - } - - var current = _voiceProxies.AddOrUpdate(guildId, - _ => new VoiceProxy(CreateVoiceGatewayLocal()), - (gid, currentProxy) => - { - _ = currentProxy.StopGateway(); - currentProxy.SetGateway(CreateVoiceGatewayLocal()); - return currentProxy; - }); - - _ = current.StartGateway(); // don't await, this blocks until gateway is closed - return current; - } - finally - { - _client.VoiceServerUpdated -= OnVoiceServerUpdated; - _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; - } - } - - public async Task JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true) - { - var gwLock = GetVoiceGatewayLock(guildId); - await gwLock.WaitAsync(); - try - { - await LeaveVoiceChannelInternalAsync(guildId); - return await InternalConnectToVcAsync(guildId, channelId); - } - finally - { - gwLock.Release(); - } - } - - public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy) - => _voiceProxies.TryGetValue(guildId, out proxy); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/IMusicService.cs b/src/Ellie.Bot.Modules.Music/Services/IMusicService.cs deleted file mode 100644 index ed4aa39..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/IMusicService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Ellie.Services.Database.Models; -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Music.Services; - -public interface IMusicService : IPlaceholderProvider -{ - /// - /// Leave voice channel in the specified guild if it's connected to one - /// - /// Id of the guild - public Task LeaveVoiceChannelAsync(ulong guildId); - - /// - /// Joins the voice channel with the specified id - /// - /// Id of the guild where the voice channel is - /// Id of the voice channel - public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId); - - Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel); - bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer); - Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer); - Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer); - Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer); - Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed); - Task PlayAsync(ulong guildId, ulong voiceChannelId); - Task> SearchVideosAsync(string query); - Task SetMusicChannelAsync(ulong guildId, ulong? channelId); - Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType); - Task SetVolumeAsync(ulong guildId, int value); - Task ToggleAutoDisconnectAsync(ulong guildId); - Task GetMusicQualityAsync(ulong guildId); - Task SetMusicQualityAsync(ulong guildId, QualityPreset preset); - Task ToggleQueueAutoPlayAsync(ulong guildId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/MusicService.cs b/src/Ellie.Bot.Modules.Music/Services/MusicService.cs deleted file mode 100644 index 3ed7870..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/MusicService.cs +++ /dev/null @@ -1,460 +0,0 @@ -using Ellie.Db; -using Ellie.Services.Database.Models; -using System.Diagnostics.CodeAnalysis; - -namespace Ellie.Modules.Music.Services; - -public sealed class MusicService : IMusicService -{ - private readonly AyuVoiceStateService _voiceStateService; - private readonly ITrackResolveProvider _trackResolveProvider; - private readonly DbService _db; - private readonly IYoutubeResolver _ytResolver; - private readonly ILocalTrackResolver _localResolver; - private readonly ISoundcloudResolver _scResolver; - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; - private readonly IGoogleApiService _googleApiService; - private readonly YtLoader _ytLoader; - private readonly IEmbedBuilderService _eb; - - private readonly ConcurrentDictionary _players; - private readonly ConcurrentDictionary _outputChannels; - private readonly ConcurrentDictionary _settings; - - public MusicService( - AyuVoiceStateService voiceStateService, - ITrackResolveProvider trackResolveProvider, - DbService db, - IYoutubeResolver ytResolver, - ILocalTrackResolver localResolver, - ISoundcloudResolver scResolver, - DiscordSocketClient client, - IBotStrings strings, - IGoogleApiService googleApiService, - YtLoader ytLoader, - IEmbedBuilderService eb) - { - _voiceStateService = voiceStateService; - _trackResolveProvider = trackResolveProvider; - _db = db; - _ytResolver = ytResolver; - _localResolver = localResolver; - _scResolver = scResolver; - _client = client; - _strings = strings; - _googleApiService = googleApiService; - _ytLoader = ytLoader; - _eb = eb; - - _players = new(); - _outputChannels = new ConcurrentDictionary(); - _settings = new(); - - _client.LeftGuild += ClientOnLeftGuild; - } - - private void DisposeMusicPlayer(IMusicPlayer musicPlayer) - { - musicPlayer.Kill(); - _ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose()); - } - - private void RemoveMusicPlayer(ulong guildId) - { - _outputChannels.TryRemove(guildId, out _); - if (_players.TryRemove(guildId, out var mp)) - DisposeMusicPlayer(mp); - } - - private Task ClientOnLeftGuild(SocketGuild guild) - { - RemoveMusicPlayer(guild.Id); - return Task.CompletedTask; - } - - public async Task LeaveVoiceChannelAsync(ulong guildId) - { - RemoveMusicPlayer(guildId); - await _voiceStateService.LeaveVoiceChannel(guildId); - } - - public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId) - => _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId); - - public async Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel) - { - var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel); - if (newPLayer is null) - return null; - - return _players.GetOrAdd(contextChannel.GuildId, newPLayer); - } - - public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer) - => _players.TryGetValue(guildId, out musicPlayer); - - public async Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer) - { - var count = 0; - await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query)) - { - if (mp.IsKilled) - break; - - mp.EnqueueTrack(track, queuer); - ++count; - } - - return count; - } - - public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer) - { - await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath)) - { - if (mp.IsKilled) - break; - - mp.EnqueueTrack(track, queuer); - } - } - - public async Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer) - { - var i = 0; - await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist)) - { - if (mp.IsKilled) - break; - - mp.EnqueueTrack(track, queuer); - ++i; - } - - return i; - } - - private async Task CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel) - { - var queue = new MusicQueue(); - var resolver = _trackResolveProvider; - - if (!_voiceStateService.TryGetProxy(guildId, out var proxy)) - return null; - - var settings = await GetSettingsInternalAsync(guildId); - - ITextChannel? overrideChannel = null; - if (settings.MusicChannelId is { } channelId) - { - overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId); - - if (overrideChannel is null) - Log.Warning("Saved music output channel doesn't exist, falling back to current channel"); - } - - _outputChannels[guildId] = (defaultChannel, overrideChannel); - - var mp = new MusicPlayer(queue, - resolver, - proxy, - _googleApiService, - settings.QualityPreset, - settings.AutoPlay); - - mp.SetRepeat(settings.PlayerRepeat); - - if (settings.Volume is >= 0 and <= 100) - mp.SetVolume(settings.Volume); - else - Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume); - - mp.OnCompleted += OnTrackCompleted(guildId); - mp.OnStarted += OnTrackStarted(guildId); - mp.OnQueueStopped += OnQueueStopped(guildId); - - return mp; - } - - public async Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed) - { - if (_outputChannels.TryGetValue(guildId, out var chan)) - { - var msg = await (chan.Override ?? chan.Default).EmbedAsync(embed); - return msg; - } - - return null; - } - - private Func OnTrackCompleted(ulong guildId) - { - IUserMessage? lastFinishedMessage = null; - return async (mp, trackInfo) => - { - _ = lastFinishedMessage?.DeleteAsync(); - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL) - .WithDescription(trackInfo.PrettyName()) - .WithFooter(trackInfo.PrettyTotalTime()); - - lastFinishedMessage = await SendToOutputAsync(guildId, embed); - }; - } - - private Func OnTrackStarted(ulong guildId) - { - IUserMessage? lastPlayingMessage = null; - return async (mp, trackInfo, index) => - { - _ = lastPlayingMessage?.DeleteAsync(); - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(guildId, strs.playing_track(index + 1)), Music.MUSIC_ICON_URL) - .WithDescription(trackInfo.PrettyName()) - .WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}"); - - lastPlayingMessage = await SendToOutputAsync(guildId, embed); - }; - } - - private Func OnQueueStopped(ulong guildId) - => _ => - { - if (_settings.TryGetValue(guildId, out var settings)) - { - if (settings.AutoDisconnect) - return LeaveVoiceChannelAsync(guildId); - } - - return Task.CompletedTask; - }; - - // this has to be done because dragging bot to another vc isn't supported yet - public async Task PlayAsync(ulong guildId, ulong voiceChannelId) - { - if (!TryGetMusicPlayer(guildId, out var mp)) - return false; - - if (mp.IsStopped) - { - if (!_voiceStateService.TryGetProxy(guildId, out var proxy) - || proxy.State == VoiceProxy.VoiceProxyState.Stopped) - await JoinVoiceChannelAsync(guildId, voiceChannelId); - } - - mp.Next(); - return true; - } - - private async Task> SearchYtLoaderVideosAsync(string query) - { - var result = await _ytLoader.LoadResultsAsync(query); - return result.Select(x => (x.Title, x.Url)).ToList(); - } - - private async Task> SearchGoogleApiVideosAsync(string query) - { - var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5); - return result.Select(x => (x.Name, x.Url)).ToList(); - } - - public async Task> SearchVideosAsync(string query) - { - try - { - IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query); - if (videos.Count > 0) - return videos; - } - catch (Exception ex) - { - Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message); - } - - try - { - return await SearchGoogleApiVideosAsync(query); - } - catch (Exception ex) - { - Log.Warning("Failed getting video results with Google Api. " - + "Probably google api key missing: {ErrorMessage}", - ex.Message); - } - - return Array.Empty<(string, string)>(); - } - - private string GetText(ulong guildId, LocStr str) - => _strings.GetText(str, guildId); - - public IEnumerable<(string Name, Func Func)> GetPlaceholders() - { - // random track that's playing - yield return ("%music.playing%", () => - { - var randomPlayingTrack = _players.Select(x => x.Value.GetCurrentTrack(out _)) - .Where(x => x is not null) - .Shuffle() - .FirstOrDefault(); - - if (randomPlayingTrack is null) - return "-"; - - return randomPlayingTrack.Title; - }); - - // number of servers currently listening to music - yield return ("%music.servers%", () => - { - var count = _players.Select(x => x.Value.GetCurrentTrack(out _)).Count(x => x is not null); - - return count.ToString(); - }); - - yield return ("%music.queued%", () => - { - var count = _players.Sum(x => x.Value.GetQueuedTracks().Count); - - return count.ToString(); - }); - } - - #region Settings - - private async Task GetSettingsInternalAsync(ulong guildId) - { - if (_settings.TryGetValue(guildId, out var settings)) - return settings; - - await using var uow = _db.GetDbContext(); - var toReturn = _settings[guildId] = await uow.Set().ForGuildAsync(guildId); - await uow.SaveChangesAsync(); - - return toReturn; - } - - private async Task ModifySettingsInternalAsync( - ulong guildId, - Action action, - TState state) - { - await using var uow = _db.GetDbContext(); - var ms = await uow.Set().ForGuildAsync(guildId); - action(ms, state); - await uow.SaveChangesAsync(); - _settings[guildId] = ms; - } - - public async Task SetMusicChannelAsync(ulong guildId, ulong? channelId) - { - if (channelId is null) - { - await UnsetMusicChannelAsync(guildId); - return true; - } - - var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value); - if (channel is null) - return false; - - await ModifySettingsInternalAsync(guildId, - (settings, chId) => - { - settings.MusicChannelId = chId; - }, - channelId); - - _outputChannels.AddOrUpdate(guildId, (channel, channel), (_, old) => (old.Default, channel)); - - return true; - } - - public async Task UnsetMusicChannelAsync(ulong guildId) - { - await ModifySettingsInternalAsync(guildId, - (settings, _) => - { - settings.MusicChannelId = null; - }, - (ulong?)null); - - if (_outputChannels.TryGetValue(guildId, out var old)) - _outputChannels[guildId] = (old.Default, null); - } - - public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType) - { - await ModifySettingsInternalAsync(guildId, - (settings, type) => - { - settings.PlayerRepeat = type; - }, - repeatType); - - if (TryGetMusicPlayer(guildId, out var mp)) - mp.SetRepeat(repeatType); - } - - public async Task SetVolumeAsync(ulong guildId, int value) - { - if (value is < 0 or > 100) - throw new ArgumentOutOfRangeException(nameof(value)); - - await ModifySettingsInternalAsync(guildId, - (settings, newValue) => - { - settings.Volume = newValue; - }, - value); - - if (TryGetMusicPlayer(guildId, out var mp)) - mp.SetVolume(value); - } - - public async Task ToggleAutoDisconnectAsync(ulong guildId) - { - var newState = false; - await ModifySettingsInternalAsync(guildId, - (settings, _) => - { - newState = settings.AutoDisconnect = !settings.AutoDisconnect; - }, - default(object)); - - return newState; - } - - public async Task GetMusicQualityAsync(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var settings = await uow.Set().ForGuildAsync(guildId); - return settings.QualityPreset; - } - - public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset) - => ModifySettingsInternalAsync(guildId, - (settings, _) => - { - settings.QualityPreset = preset; - }, - preset); - - public async Task ToggleQueueAutoPlayAsync(ulong guildId) - { - var newValue = false; - await ModifySettingsInternalAsync(guildId, - (settings, _) => newValue = settings.AutoPlay = !settings.AutoPlay, - false); - - if (TryGetMusicPlayer(guildId, out var mp)) - mp.AutoPlay = newValue; - - return newValue; - } - - #endregion -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/SoundCloudApiService.cs b/src/Ellie.Bot.Modules.Music/Services/SoundCloudApiService.cs deleted file mode 100644 index 3f1df4b..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/SoundCloudApiService.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Services; - -public class SoundCloudApiService : IEService -{ - private readonly IHttpClientFactory _httpFactory; - - public SoundCloudApiService(IHttpClientFactory factory) - => _httpFactory = factory; - - public async Task ResolveVideoAsync(string url) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentNullException(nameof(url)); - - var response = string.Empty; - - using (var http = _httpFactory.CreateClient()) - { - response = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={url}"); - } - - var responseObj = JsonConvert.DeserializeObject(response); - if (responseObj?.Kind != "track") - throw new InvalidOperationException("Url is either not a track, or it doesn't exist."); - - return responseObj; - } - - public async Task GetVideoByQueryAsync(string query) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); - - var response = string.Empty; - using (var http = _httpFactory.CreateClient()) - { - response = await http.GetStringAsync( - new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}")); - } - - var responseObj = JsonConvert.DeserializeObject(response) - .FirstOrDefault(s => s.Streamable is true); - - if (responseObj?.Kind != "track") - throw new InvalidOperationException("Query yielded no results."); - - return responseObj; - } -} - -public class SoundCloudVideo -{ - public string Kind { get; set; } = string.Empty; - public long Id { get; set; } = 0; - public SoundCloudUser User { get; set; } = new(); - public string Title { get; set; } = string.Empty; - - public string FullName - => User.Name + " - " + Title; - - public bool? Streamable { get; set; } = false; - public int Duration { get; set; } - - [JsonProperty("permalink_url")] - public string TrackLink { get; set; } = string.Empty; - - [JsonProperty("artwork_url")] - public string ArtworkUrl { get; set; } = string.Empty; -} - -public class SoundCloudUser -{ - [JsonProperty("username")] - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/extractor/Misc.cs b/src/Ellie.Bot.Modules.Music/Services/extractor/Misc.cs deleted file mode 100644 index 7e2fca0..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/extractor/Misc.cs +++ /dev/null @@ -1,71 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music.Services; - -public sealed partial class YtLoader -{ - public class InitRange - { - public string Start { get; set; } - public string End { get; set; } - } - - public class IndexRange - { - public string Start { get; set; } - public string End { get; set; } - } - - public class ColorInfo - { - public string Primaries { get; set; } - public string TransferCharacteristics { get; set; } - public string MatrixCoefficients { get; set; } - } - - public class YtAdaptiveFormat - { - public int Itag { get; set; } - public string MimeType { get; set; } - public int Bitrate { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public InitRange InitRange { get; set; } - public IndexRange IndexRange { get; set; } - public string LastModified { get; set; } - public string ContentLength { get; set; } - public string Quality { get; set; } - public int Fps { get; set; } - public string QualityLabel { get; set; } - public string ProjectionType { get; set; } - public int AverageBitrate { get; set; } - public ColorInfo ColorInfo { get; set; } - public string ApproxDurationMs { get; set; } - public string SignatureCipher { get; set; } - } - - public abstract class TrackInfo - { - public abstract string Url { get; } - public abstract string Title { get; } - public abstract TimeSpan Duration { get; } - } - - public sealed class YtTrackInfo : TrackInfo - { - private const string BASE_YOUTUBE_URL = "https://youtube.com/watch?v="; - public override string Url { get; } - public override string Title { get; } - public override TimeSpan Duration { get; } - - private readonly string _videoId; - - public YtTrackInfo(string title, string videoId, TimeSpan duration) - { - Title = title; - Url = BASE_YOUTUBE_URL + videoId; - Duration = duration; - - _videoId = videoId; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/Services/extractor/YtLoader.cs b/src/Ellie.Bot.Modules.Music/Services/extractor/YtLoader.cs deleted file mode 100644 index 2f1e38b..0000000 --- a/src/Ellie.Bot.Modules.Music/Services/extractor/YtLoader.cs +++ /dev/null @@ -1,130 +0,0 @@ -#nullable disable -using System.Globalization; -using System.Text; -using System.Text.Json; - -namespace Ellie.Modules.Music.Services; - -public sealed partial class YtLoader -{ - private static readonly byte[] _ytResultInitialData = Encoding.UTF8.GetBytes("var ytInitialData = "); - private static readonly byte[] _ytResultJsonEnd = Encoding.UTF8.GetBytes(";<"); - - private static readonly string[] _durationFormats = - { - @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss" - }; - - private readonly IHttpClientFactory _httpFactory; - - public YtLoader(IHttpClientFactory httpFactory) - => _httpFactory = httpFactory; - - // public async Task LoadTrackByIdAsync(string videoId) - // { - // using var http = new HttpClient(); - // http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1"); - // http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00"); - // http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); - // - // var responseString = await http.GetStringAsync($"https://youtube.com?" + - // $"pbj=1" + - // $"&hl=en" + - // $"&v=" + videoId); - // - // var jsonDoc = JsonDocument.Parse(responseString).RootElement; - // var elem = jsonDoc.EnumerateArray() - // .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch"); - // - // var formatsJsonArray = elem.GetProperty("streamingdata") - // .GetProperty("formats") - // .GetRawText(); - // - // var formats = JsonSerializer.Deserialize>(formatsJsonArray); - // var result = formats - // .Where(x => x.MimeType.StartsWith("audio/")) - // .OrderByDescending(x => x.Bitrate) - // .FirstOrDefault(); - // - // if (result is null) - // return null; - // - // return new YtTrackInfo("1", "2", TimeSpan.Zero); - // } - - public async Task> LoadResultsAsync(string query) - { - query = Uri.EscapeDataString(query); - - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); - - byte[] response; - try - { - response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}"); - } - catch (HttpRequestException ex) - { - Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message); - return null; - } - - // there is a lot of useless html above the script tag, however if html gets significantly reduced - // this will result in the json being cut off - - var mem = GetScriptResponseSpan(response); - var root = JsonDocument.Parse(mem).RootElement; - - using var tracksJsonItems = root - .GetProperty("contents") - .GetProperty("twoColumnSearchResultsRenderer") - .GetProperty("primaryContents") - .GetProperty("sectionListRenderer") - .GetProperty("contents")[0] - .GetProperty("itemSectionRenderer") - .GetProperty("contents") - .EnumerateArray(); - - var tracks = new List(); - foreach (var track in tracksJsonItems) - { - if (!track.TryGetProperty("videoRenderer", out var elem)) - continue; - - var videoId = elem.GetProperty("videoId").GetString(); - // var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString(); - var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString(); - var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString(); - - if (!TimeSpan.TryParseExact(durationString, - _durationFormats, - CultureInfo.InvariantCulture, - out var duration)) - { - Log.Warning("Cannot parse duration: {DurationString}", durationString); - continue; - } - - tracks.Add(new YtTrackInfo(title, videoId, duration)); - if (tracks.Count >= 5) - break; - } - - return tracks; - } - - private Memory GetScriptResponseSpan(byte[] response) - { - var responseSpan = response.AsSpan()[140_000..]; - var startIndex = responseSpan.IndexOf(_ytResultInitialData); - if (startIndex == -1) - return null; // FUTURE try selecting html - startIndex += _ytResultInitialData.Length; - - var endIndex = - 140_000 + startIndex + responseSpan[(startIndex + 20_000)..].IndexOf(_ytResultJsonEnd) + 20_000; - startIndex += 140_000; - return response.AsMemory(startIndex, endIndex - startIndex); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ICachableTrackData.cs b/src/Ellie.Bot.Modules.Music/_common/ICachableTrackData.cs deleted file mode 100644 index 0e55af8..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ICachableTrackData.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public interface ICachableTrackData -{ - string Id { get; set; } - string Url { get; set; } - string Thumbnail { get; set; } - public TimeSpan Duration { get; } - MusicPlatform Platform { get; set; } - string Title { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ILocalTrackResolver.cs b/src/Ellie.Bot.Modules.Music/_common/ILocalTrackResolver.cs deleted file mode 100644 index 7de569a..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ILocalTrackResolver.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public interface ILocalTrackResolver : IPlatformQueryResolver -{ - IAsyncEnumerable ResolveDirectoryAsync(string dirPath); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IMusicPlayer.cs b/src/Ellie.Bot.Modules.Music/_common/IMusicPlayer.cs deleted file mode 100644 index 2f92161..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IMusicPlayer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Music; - -public interface IMusicPlayer : IDisposable -{ - float Volume { get; } - bool IsPaused { get; } - bool IsStopped { get; } - bool IsKilled { get; } - int CurrentIndex { get; } - public PlayerRepeatType Repeat { get; } - bool AutoPlay { get; set; } - - void Stop(); - void Clear(); - IReadOnlyCollection GetQueuedTracks(); - IQueuedTrackInfo? GetCurrentTrack(out int index); - void Next(); - bool MoveTo(int index); - void SetVolume(int newVolume); - - void Kill(); - bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo); - - - Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync( - string query, - string queuer, - bool asNext, - MusicPlatform? forcePlatform = null); - - Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer); - bool TogglePause(); - IQueuedTrackInfo? MoveTrack(int from, int to); - void EnqueueTrack(ITrackInfo track, string queuer); - void EnqueueTracks(IEnumerable tracks, string queuer); - void SetRepeat(PlayerRepeatType type); - void ShuffleQueue(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IMusicQueue.cs b/src/Ellie.Bot.Modules.Music/_common/IMusicQueue.cs deleted file mode 100644 index 7c6fe92..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IMusicQueue.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Ellie.Modules.Music; - -public interface IMusicQueue -{ - int Index { get; } - int Count { get; } - IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index); - IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index); - - void EnqueueMany(IEnumerable tracks, string queuer); - - public IReadOnlyCollection List(); - IQueuedTrackInfo? GetCurrent(out int index); - void Advance(); - void Clear(); - bool SetIndex(int index); - bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent); - void RemoveCurrent(); - IQueuedTrackInfo? MoveTrack(int from, int to); - void Shuffle(Random rng); - bool IsLast(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IPlatformQueryResolver.cs b/src/Ellie.Bot.Modules.Music/_common/IPlatformQueryResolver.cs deleted file mode 100644 index cb2abc6..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IPlatformQueryResolver.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Modules.Music; - -public interface IPlatformQueryResolver -{ - Task ResolveByQueryAsync(string query); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IQueuedTrackInfo.cs b/src/Ellie.Bot.Modules.Music/_common/IQueuedTrackInfo.cs deleted file mode 100644 index 855c2c8..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IQueuedTrackInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public interface IQueuedTrackInfo : ITrackInfo -{ - public ITrackInfo TrackInfo { get; } - - public string Queuer { get; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IRadioResolver.cs b/src/Ellie.Bot.Modules.Music/_common/IRadioResolver.cs deleted file mode 100644 index e095e94..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IRadioResolver.cs +++ /dev/null @@ -1,6 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public interface IRadioResolver : IPlatformQueryResolver -{ -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ISoundcloudResolver.cs b/src/Ellie.Bot.Modules.Music/_common/ISoundcloudResolver.cs deleted file mode 100644 index b0d421b..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ISoundcloudResolver.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public interface ISoundcloudResolver : IPlatformQueryResolver -{ - bool IsSoundCloudLink(string url); - IAsyncEnumerable ResolvePlaylistAsync(string playlist); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ITrackCacher.cs b/src/Ellie.Bot.Modules.Music/_common/ITrackCacher.cs deleted file mode 100644 index 78a9dbc..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ITrackCacher.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ellie.Modules.Music; - -public interface ITrackCacher -{ - Task GetOrCreateStreamLink( - string id, - MusicPlatform platform, - Func> streamUrlFactory); - - Task CacheTrackDataAsync(ICachableTrackData data); - Task GetCachedDataByIdAsync(string id, MusicPlatform platform); - Task GetCachedDataByQueryAsync(string query, MusicPlatform platform); - Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data); - - Task CacheStreamUrlAsync( - string id, - MusicPlatform platform, - string url, - TimeSpan expiry); - - Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform); - Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids); - Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId); - Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ITrackInfo.cs b/src/Ellie.Bot.Modules.Music/_common/ITrackInfo.cs deleted file mode 100644 index 2c37717..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ITrackInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ellie.Modules.Music; - -public interface ITrackInfo -{ - public string Id => string.Empty; - public string Title { get; } - public string Url { get; } - public string Thumbnail { get; } - public TimeSpan Duration { get; } - public MusicPlatform Platform { get; } - public ValueTask GetStreamUrl(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/ITrackResolveProvider.cs b/src/Ellie.Bot.Modules.Music/_common/ITrackResolveProvider.cs deleted file mode 100644 index cc069c9..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/ITrackResolveProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ellie.Modules.Music; - -public interface ITrackResolveProvider -{ - Task QuerySongAsync(string query, MusicPlatform? forcePlatform); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IVoiceProxy.cs b/src/Ellie.Bot.Modules.Music/_common/IVoiceProxy.cs deleted file mode 100644 index e5440e8..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IVoiceProxy.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -using Ayu.Discord.Voice; - -namespace Ellie.Modules.Music; - -public interface IVoiceProxy -{ - VoiceProxy.VoiceProxyState State { get; } - public bool SendPcmFrame(VoiceClient vc, Span data, int length); - public void SetGateway(VoiceGateway gateway); - Task StartSpeakingAsync(); - Task StopSpeakingAsync(); - public Task StartGateway(); - Task StopGateway(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/IYoutubeResolver.cs b/src/Ellie.Bot.Modules.Music/_common/IYoutubeResolver.cs deleted file mode 100644 index 0116460..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/IYoutubeResolver.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Ellie.Modules.Music; - -public interface IYoutubeResolver : IPlatformQueryResolver -{ - public Regex YtVideoIdRegex { get; } - public Task ResolveByIdAsync(string id); - IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query); - Task ResolveByQueryAsync(string query, bool tryExtractingId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/CachableTrackData.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/CachableTrackData.cs deleted file mode 100644 index 5cca98f..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/CachableTrackData.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Music; - -public sealed class CachableTrackData : ICachableTrackData -{ - public string Title { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - public string Url { get; set; } = string.Empty; - public string Thumbnail { get; set; } = string.Empty; - public double TotalDurationMs { get; set; } - - [JsonIgnore] - public TimeSpan Duration - => TimeSpan.FromMilliseconds(TotalDurationMs); - - public MusicPlatform Platform { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/MultimediaTimer.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/MultimediaTimer.cs deleted file mode 100644 index 4ef5d3d..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/MultimediaTimer.cs +++ /dev/null @@ -1,95 +0,0 @@ -#nullable disable -using System.Runtime.InteropServices; - -namespace Ellie.Modules.Music.Common; - -public sealed class MultimediaTimer : IDisposable -{ - private LpTimeProcDelegate lpTimeProc; - private readonly uint _eventId; - private readonly Action _callback; - private readonly object _state; - - public MultimediaTimer(Action callback, object state, int period) - { - if (period <= 0) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); - - _callback = callback; - _state = state; - - lpTimeProc = CallbackInternal; - _eventId = timeSetEvent((uint)period, 1, lpTimeProc, 0, TimerMode.Periodic); - } - - /// - /// The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread. - /// After the event is activated, it calls the specified callback function or sets or pulses the specified - /// event object. - /// - /// - /// Event delay, in milliseconds. If this value is not in the range of the minimum and - /// maximum event delays supported by the timer, the function returns an error. - /// - /// - /// Resolution of the timer event, in milliseconds. The resolution increases with - /// smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy. - /// To reduce system overhead, however, you should use the maximum value appropriate for your application. - /// - /// - /// Pointer to a callback function that is called once upon expiration of a single event or periodically upon - /// expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE - /// flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or - /// pulsed upon completion of a single event or periodically upon completion of periodic events. - /// For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type - /// LPTIMECALLBACK. - /// - /// User-supplied callback data. - /// - /// Timer event type. This parameter may include one of the following values. - [DllImport("Winmm.dll")] - private static extern uint timeSetEvent( - uint uDelay, - uint uResolution, - LpTimeProcDelegate lpTimeProc, - int dwUser, - TimerMode fuEvent); - - /// - /// The timeKillEvent function cancels a specified timer event. - /// - /// - /// Identifier of the timer event to cancel. - /// This identifier was returned by the timeSetEvent function when the timer event was set up. - /// - /// Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist. - [DllImport("Winmm.dll")] - private static extern int timeKillEvent(uint uTimerId); - - private void CallbackInternal( - uint uTimerId, - uint uMsg, - int dwUser, - int dw1, - int dw2) - => _callback(_state); - - public void Dispose() - { - lpTimeProc = default; - timeKillEvent(_eventId); - } - - private delegate void LpTimeProcDelegate( - uint uTimerId, - uint uMsg, - int dwUser, - int dw1, - int dw2); - - private enum TimerMode - { - OneShot, - Periodic - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicExtensions.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/MusicExtensions.cs deleted file mode 100644 index a123fa2..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public static class MusicExtensions -{ - public static string PrettyTotalTime(this IMusicPlayer mp) - { - long sum = 0; - foreach (var track in mp.GetQueuedTracks()) - { - if (track.Duration == TimeSpan.MaxValue) - return "∞"; - - sum += track.Duration.Ticks; - } - - var total = new TimeSpan(sum); - - return total.ToString(@"hh\:mm\:ss"); - } - - public static string PrettyVolume(this IMusicPlayer mp) - => $"🔉 {(int)(mp.Volume * 100)}%"; - - public static string PrettyName(this ITrackInfo trackInfo) - => $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**"; - - public static string PrettyInfo(this IQueuedTrackInfo trackInfo) - => $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}"; - - public static string PrettyFullName(this IQueuedTrackInfo trackInfo) - => $@"{trackInfo.PrettyName()} - `{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {Format.Sanitize(trackInfo.Queuer.TrimTo(15))}`"; - - public static string PrettyTotalTime(this ITrackInfo trackInfo) - { - if (trackInfo.Duration == TimeSpan.Zero) - return "(?)"; - if (trackInfo.Duration == TimeSpan.MaxValue) - return "∞"; - if (trackInfo.Duration.TotalHours >= 1) - return trackInfo.Duration.ToString("""hh\:mm\:ss"""); - - return trackInfo.Duration.ToString("""mm\:ss"""); - } - - public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id) - => new CachableTrackData - { - TotalDurationMs = trackInfo.Duration.TotalMilliseconds, - Id = id, - Thumbnail = trackInfo.Thumbnail, - Url = trackInfo.Url, - Platform = trackInfo.Platform, - Title = trackInfo.Title - }; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlatform.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlatform.cs deleted file mode 100644 index c34eb53..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlatform.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Music; - -public enum MusicPlatform -{ - Radio, - Youtube, - Local, - SoundCloud -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlayer.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlayer.cs deleted file mode 100644 index 9e87b13..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicPlayer.cs +++ /dev/null @@ -1,528 +0,0 @@ -using Ayu.Discord.Voice; -using Ellie.Services.Database.Models; -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Ellie.Modules.Music; - -public sealed class MusicPlayer : IMusicPlayer -{ - public event Func? OnCompleted; - public event Func? OnStarted; - public event Func? OnQueueStopped; - public bool IsKilled { get; private set; } - public bool IsStopped { get; private set; } - public bool IsPaused { get; private set; } - public PlayerRepeatType Repeat { get; private set; } - - public int CurrentIndex - => _queue.Index; - - public float Volume { get; private set; } = 1.0f; - - private readonly AdjustVolumeDelegate _adjustVolume; - private readonly VoiceClient _vc; - - private readonly IMusicQueue _queue; - private readonly ITrackResolveProvider _trackResolveProvider; - private readonly IVoiceProxy _proxy; - private readonly IGoogleApiService _googleApiService; - private readonly ISongBuffer _songBuffer; - - private bool skipped; - private int? forceIndex; - private readonly Thread _thread; - private readonly Random _rng; - - public bool AutoPlay { get; set; } - - public MusicPlayer( - IMusicQueue queue, - ITrackResolveProvider trackResolveProvider, - IVoiceProxy proxy, - IGoogleApiService googleApiService, - QualityPreset qualityPreset, - bool autoPlay) - { - _queue = queue; - _trackResolveProvider = trackResolveProvider; - _proxy = proxy; - _googleApiService = googleApiService; - AutoPlay = autoPlay; - _rng = new EllieRandom(); - - _vc = GetVoiceClient(qualityPreset); - if (_vc.BitDepth == 16) - _adjustVolume = AdjustVolumeInt16; - else - _adjustVolume = AdjustVolumeFloat32; - - _songBuffer = new PoopyBufferImmortalized(_vc.InputLength); - - _thread = new(async () => - { - await PlayLoop(); - }); - _thread.Start(); - } - - private static VoiceClient GetVoiceClient(QualityPreset qualityPreset) - => qualityPreset switch - { - QualityPreset.Highest => new(), - QualityPreset.High => new(SampleRate._48k, Bitrate._128k, Channels.Two, FrameDelay.Delay40), - QualityPreset.Medium => new(SampleRate._48k, - Bitrate._96k, - Channels.Two, - FrameDelay.Delay40, - BitDepthEnum.UInt16), - QualityPreset.Low => new(SampleRate._48k, - Bitrate._64k, - Channels.Two, - FrameDelay.Delay40, - BitDepthEnum.UInt16), - _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null) - }; - - private async Task PlayLoop() - { - var sw = new Stopwatch(); - - while (!IsKilled) - { - // wait until a song is available in the queue - // or until the queue is resumed - var track = _queue.GetCurrent(out var index); - - if (track is null || IsStopped) - { - await Task.Delay(500); - continue; - } - - if (skipped) - { - skipped = false; - _queue.Advance(); - continue; - } - - using var cancellationTokenSource = new CancellationTokenSource(); - var token = cancellationTokenSource.Token; - try - { - // light up green in vc - _ = _proxy.StartSpeakingAsync(); - - _ = OnStarted?.Invoke(this, track, index); - - // make sure song buffer is ready to be (re)used - _songBuffer.Reset(); - - var streamUrl = await track.GetStreamUrl(); - // start up the data source - using var source = FfmpegTrackDataSource.CreateAsync( - _vc.BitDepth, - streamUrl, - track.Platform == MusicPlatform.Local); - - // start moving data from the source into the buffer - // this method will return once the sufficient prebuffering is done - await _songBuffer.BufferAsync(source, token); - - // // Implemenation with multimedia timer. Works but a hassle because no support for switching - // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option - // // for selfhosters. - // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - // { - // var cancelSource = new CancellationTokenSource(); - // var cancelToken = cancelSource.Token; - // using var timer = new MultimediaTimer(_ => - // { - // if (IsStopped || IsKilled) - // { - // cancelSource.Cancel(); - // return; - // } - // - // if (_skipped) - // { - // _skipped = false; - // cancelSource.Cancel(); - // return; - // } - // - // if (IsPaused) - // return; - // - // try - // { - // // this should tolerate certain number of errors - // var result = CopyChunkToOutput(_songBuffer, _vc); - // if (!result) - // cancelSource.Cancel(); - // - // } - // catch (Exception ex) - // { - // Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); - // cancelSource.Cancel(); - // } - // - // }, null, 20); - // - // while(true) - // await Task.Delay(1000, cancelToken); - // } - - // start sending data - var ticksPerMs = 1000f / Stopwatch.Frequency; - sw.Start(); - Thread.Sleep(2); - - var delay = sw.ElapsedTicks * ticksPerMs > 3f ? _vc.Delay - 16 : _vc.Delay - 3; - - var errorCount = 0; - while (!IsStopped && !IsKilled) - { - // doing the skip this way instead of in the condition - // ensures that a song will for sure be skipped - if (skipped) - { - skipped = false; - break; - } - - if (IsPaused) - { - await Task.Delay(200); - continue; - } - - sw.Restart(); - var ticks = sw.ElapsedTicks; - try - { - var result = CopyChunkToOutput(_songBuffer, _vc); - - // if song is finished - if (result is null) - break; - - if (result is true) - { - if (errorCount > 0) - { - _ = _proxy.StartSpeakingAsync(); - errorCount = 0; - } - - // FUTURE windows multimedia api - - // wait for slightly less than the latency - Thread.Sleep(delay); - - // and then spin out the rest - while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f) - Thread.SpinWait(100); - } - else - { - // result is false is either when the gateway is being swapped - // or if the bot is reconnecting, or just disconnected for whatever reason - - // tolerate up to 15x200ms of failures (3 seconds) - if (++errorCount <= 15) - { - await Task.Delay(200); - continue; - } - - Log.Warning("Can't send data to voice channel"); - - IsStopped = true; - // if errors are happening for more than 3 seconds - // Stop the player - break; - } - } - catch (Exception ex) - { - Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); - } - } - } - catch (Win32Exception) - { - IsStopped = true; - Log.Error("Please install ffmpeg and make sure it's added to your " - + "PATH environment variable before trying again"); - } - catch (OperationCanceledException) - { - Log.Information("Song skipped"); - } - catch (Exception ex) - { - Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message); - } - finally - { - cancellationTokenSource.Cancel(); - // turn off green in vc - - _ = OnCompleted?.Invoke(this, track); - - if (AutoPlay && track.Platform == MusicPlatform.Youtube) - { - try - { - var relatedSongs = await _googleApiService.GetRelatedVideosAsync(track.TrackInfo.Id, 5); - var related = relatedSongs.Shuffle().FirstOrDefault(); - if (related is not null) - { - var relatedTrack = await _trackResolveProvider.QuerySongAsync(related, MusicPlatform.Youtube); - if (relatedTrack is not null) - EnqueueTrack(relatedTrack, "Autoplay"); - } - } - catch (Exception ex) - { - Log.Warning(ex, "Failed queueing a related song via autoplay"); - } - } - - - HandleQueuePostTrack(); - skipped = false; - - _ = _proxy.StopSpeakingAsync(); - - await Task.Delay(100); - } - } - } - - private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc) - { - var data = sb.Read(vc.InputLength, out var length); - - // if nothing is read from the buffer, song is finished - if (data.Length == 0) - return null; - - _adjustVolume(data, Volume); - return _proxy.SendPcmFrame(vc, data, length); - } - - private void HandleQueuePostTrack() - { - if (forceIndex is { } index) - { - _queue.SetIndex(index); - forceIndex = null; - return; - } - - var (repeat, isStopped) = (Repeat, IsStopped); - - if (repeat == PlayerRepeatType.Track || isStopped) - return; - - // if queue is being repeated, advance no matter what - if (repeat == PlayerRepeatType.None) - { - // if this is the last song, - // stop the queue - if (_queue.IsLast()) - { - IsStopped = true; - OnQueueStopped?.Invoke(this); - return; - } - - _queue.Advance(); - return; - } - - _queue.Advance(); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AdjustVolumeInt16(Span audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) - return; - - var samples = MemoryMarshal.Cast(audioSamples); - - for (var i = 0; i < samples.Length; i++) - { - ref var sample = ref samples[i]; - sample = (short)(sample * volume); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AdjustVolumeFloat32(Span audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) - return; - - var samples = MemoryMarshal.Cast(audioSamples); - - for (var i = 0; i < samples.Length; i++) - { - ref var sample = ref samples[i]; - sample *= volume; - } - } - - public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync( - string query, - string queuer, - bool asNext, - MusicPlatform? forcePlatform = null) - { - var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform); - if (song is null) - return default; - - int index; - - if (asNext) - return (_queue.EnqueueNext(song, queuer, out index), index); - - return (_queue.Enqueue(song, queuer, out index), index); - } - - public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer) - { - var errorCount = 0; - foreach (var chunk in queries.Chunk(5)) - { - if (IsKilled) - break; - - await chunk.Select(async data => - { - var (query, platform) = data; - try - { - await TryEnqueueTrackAsync(query, queuer, false, platform); - errorCount = 0; - } - catch (Exception ex) - { - Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query); - ++errorCount; - } - }) - .WhenAll(); - - await Task.Delay(1000); - - // > 10 errors in a row = kill - if (errorCount > 10) - break; - } - } - - public void EnqueueTrack(ITrackInfo track, string queuer) - => _queue.Enqueue(track, queuer, out _); - - public void EnqueueTracks(IEnumerable tracks, string queuer) - => _queue.EnqueueMany(tracks, queuer); - - public void SetRepeat(PlayerRepeatType type) - => Repeat = type; - - public void ShuffleQueue() - => _queue.Shuffle(_rng); - - public void Stop() - => IsStopped = true; - - public void Clear() - { - _queue.Clear(); - skipped = true; - } - - public IReadOnlyCollection GetQueuedTracks() - => _queue.List(); - - public IQueuedTrackInfo? GetCurrentTrack(out int index) - => _queue.GetCurrent(out index); - - public void Next() - { - skipped = true; - IsStopped = false; - IsPaused = false; - } - - public bool MoveTo(int index) - { - if (_queue.SetIndex(index)) - { - forceIndex = index; - skipped = true; - IsStopped = false; - IsPaused = false; - return true; - } - - return false; - } - - public void SetVolume(int newVolume) - { - var normalizedVolume = newVolume / 100f; - if (normalizedVolume is < 0f or > 1f) - throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100"); - - Volume = normalizedVolume; - } - - public void Kill() - { - IsKilled = true; - IsStopped = true; - IsPaused = false; - skipped = true; - } - - public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo) - { - if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent)) - return false; - - if (isCurrent) - skipped = true; - - return true; - } - - public bool TogglePause() - => IsPaused = !IsPaused; - - public IQueuedTrackInfo? MoveTrack(int from, int to) - => _queue.MoveTrack(from, to); - - public void Dispose() - { - IsKilled = true; - OnCompleted = null; - OnStarted = null; - OnQueueStopped = null; - _queue.Clear(); - _songBuffer.Dispose(); - _vc.Dispose(); - } - - private delegate void AdjustVolumeDelegate(Span data, float volume); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicQueue.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/MusicQueue.cs deleted file mode 100644 index ef4a9a8..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/MusicQueue.cs +++ /dev/null @@ -1,316 +0,0 @@ -namespace Ellie.Modules.Music; - -public sealed partial class MusicQueue -{ - private sealed class QueuedTrackInfo : IQueuedTrackInfo - { - public ITrackInfo TrackInfo { get; } - public string Queuer { get; } - - public string Title - => TrackInfo.Title; - - public string Url - => TrackInfo.Url; - - public string Thumbnail - => TrackInfo.Thumbnail; - - public TimeSpan Duration - => TrackInfo.Duration; - - public MusicPlatform Platform - => TrackInfo.Platform; - - - public QueuedTrackInfo(ITrackInfo trackInfo, string queuer) - { - TrackInfo = trackInfo; - Queuer = queuer; - } - - public ValueTask GetStreamUrl() - => TrackInfo.GetStreamUrl(); - } -} - -public sealed partial class MusicQueue : IMusicQueue -{ - public int Index - { - get - { - // just make sure the internal logic runs first - // to make sure that some potential indermediate value is not returned - lock (_locker) - { - return index; - } - } - } - - public int Count - { - get - { - lock (_locker) - { - return tracks.Count; - } - } - } - - private LinkedList tracks; - - private int index; - - private readonly object _locker = new(); - - public MusicQueue() - { - index = 0; - tracks = new(); - } - - public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int enqueuedAt) - { - lock (_locker) - { - var added = new QueuedTrackInfo(trackInfo, queuer); - enqueuedAt = tracks.Count; - tracks.AddLast(added); - return added; - } - } - - public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int trackIndex) - { - lock (_locker) - { - if (tracks.Count == 0) - return Enqueue(trackInfo, queuer, out trackIndex); - - var currentNode = tracks.First!; - int i; - for (i = 1; i <= index; i++) - currentNode = currentNode.Next!; // can't be null because index is always in range of the count - - var added = new QueuedTrackInfo(trackInfo, queuer); - trackIndex = i; - - tracks.AddAfter(currentNode, added); - - return added; - } - } - - public void EnqueueMany(IEnumerable toEnqueue, string queuer) - { - lock (_locker) - { - foreach (var track in toEnqueue) - { - var added = new QueuedTrackInfo(track, queuer); - tracks.AddLast(added); - } - } - } - - public IReadOnlyCollection List() - { - lock (_locker) - { - return tracks.ToList(); - } - } - - public IQueuedTrackInfo? GetCurrent(out int currentIndex) - { - lock (_locker) - { - currentIndex = index; - return tracks.ElementAtOrDefault(index); - } - } - - public void Advance() - { - lock (_locker) - { - if (++index >= tracks.Count) - index = 0; - } - } - - public void Clear() - { - lock (_locker) - { - tracks.Clear(); - } - } - - public bool SetIndex(int newIndex) - { - lock (_locker) - { - if (newIndex < 0 || newIndex >= tracks.Count) - return false; - - index = newIndex; - return true; - } - } - - private void RemoveAtInternal(int remoteAtIndex, out IQueuedTrackInfo trackInfo) - { - var removedNode = tracks.First!; - int i; - for (i = 0; i < remoteAtIndex; i++) - removedNode = removedNode.Next!; - - trackInfo = removedNode.Value; - tracks.Remove(removedNode); - - if (i <= index) - --index; - - if (index < 0) - index = Count; - - // if it was the last song in the queue - // // wrap back to start - // if (_index == Count) - // _index = 0; - // else if (i <= _index) - // if (_index == 0) - // _index = Count; - // else --_index; - } - - public void RemoveCurrent() - { - lock (_locker) - { - if (index < tracks.Count) - RemoveAtInternal(index, out _); - } - } - - public IQueuedTrackInfo? MoveTrack(int from, int to) - { - if (from < 0) - throw new ArgumentOutOfRangeException(nameof(from)); - if (to < 0) - throw new ArgumentOutOfRangeException(nameof(to)); - if (to == from) - throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different"); - - lock (_locker) - { - if (from >= Count || to >= Count) - return null; - - // update current track index - if (from == index) - { - // if the song being moved is the current track - // it means that it will for sure end up on the destination - index = to; - } - else - { - // moving a track from below the current track means - // means it will drop down - if (from < index) - index--; - - // moving a track to below the current track - // means it will rise up - if (to <= index) - index++; - - - // if both from and to are below _index - net change is + 1 - 1 = 0 - // if from is below and to is above - net change is -1 (as the track is taken and put above) - // if from is above and to is below - net change is 1 (as the track is inserted under) - // if from is above and to is above - net change is 0 - } - - // get the node which needs to be moved - var fromNode = tracks.First!; - for (var i = 0; i < from; i++) - fromNode = fromNode.Next!; - - // remove it from the queue - tracks.Remove(fromNode); - - // if it needs to be added as a first node, - // add it directly and return - if (to == 0) - { - tracks.AddFirst(fromNode); - return fromNode.Value; - } - - // else find the node at the index before the specified target - var addAfterNode = tracks.First!; - for (var i = 1; i < to; i++) - addAfterNode = addAfterNode.Next!; - - // and add after it - tracks.AddAfter(addAfterNode, fromNode); - return fromNode.Value; - } - } - - public void Shuffle(Random rng) - { - lock (_locker) - { - var list = tracks.ToList(); - - for (var i = 0; i < list.Count; i++) - { - var struck = rng.Next(i, list.Count); - (list[struck], list[i]) = (list[i], list[struck]); - - // could preserving the index during shuffling be done better? - if (i == index) - index = struck; - else if (struck == index) - index = i; - } - - tracks = new(list); - } - } - - public bool IsLast() - { - lock (_locker) - { - return index == tracks.Count // if there are no tracks - || index == tracks.Count - 1; - } - } - - public bool TryRemoveAt(int remoteAt, out IQueuedTrackInfo? trackInfo, out bool isCurrent) - { - lock (_locker) - { - isCurrent = false; - trackInfo = null; - - if (remoteAt < 0 || remoteAt >= tracks.Count) - return false; - - if (remoteAt == index) - isCurrent = true; - - RemoveAtInternal(remoteAt, out trackInfo); - - return true; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/RemoteTrackInfo.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/RemoteTrackInfo.cs deleted file mode 100644 index 105906a..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/RemoteTrackInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Ellie.Modules.Music; - -public sealed record RemoteTrackInfo( - string Id, - string Title, - string Url, - string Thumbnail, - TimeSpan Duration, - MusicPlatform Platform, - Func> _streamFactory) : ITrackInfo -{ - private readonly Func> _streamFactory = _streamFactory; - - public async ValueTask GetStreamUrl() - => await _streamFactory(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/SimpleTrackInfo.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/SimpleTrackInfo.cs deleted file mode 100644 index 1ea7568..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/SimpleTrackInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Ellie.Modules.Music; - -public sealed class SimpleTrackInfo : ITrackInfo -{ - public string Title { get; } - public string Url { get; } - public string Thumbnail { get; } - public TimeSpan Duration { get; } - public MusicPlatform Platform { get; } - public string? StreamUrl { get; } - - public SimpleTrackInfo( - string title, - string url, - string thumbnail, - TimeSpan duration, - MusicPlatform platform, - string streamUrl) - { - Title = title; - Url = url; - Thumbnail = thumbnail; - Duration = duration; - Platform = platform; - StreamUrl = streamUrl; - } - - public ValueTask GetStreamUrl() - => new(StreamUrl); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/TrackCacher.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/TrackCacher.cs deleted file mode 100644 index 8369832..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/TrackCacher.cs +++ /dev/null @@ -1,105 +0,0 @@ -namespace Ellie.Modules.Music; - -public sealed class TrackCacher : ITrackCacher -{ - private readonly IBotCache _cache; - - public TrackCacher(IBotCache cache) - => _cache = cache; - - - private TypedKey GetStreamLinkKey(MusicPlatform platform, string id) - => new($"music:stream:{platform}:{id}"); - - public async Task GetOrCreateStreamLink( - string id, - MusicPlatform platform, - Func> streamUrlFactory) - { - var key = GetStreamLinkKey(platform, id); - - var streamUrl = await _cache.GetOrDefaultAsync(key); - await _cache.RemoveAsync(key); - - if (streamUrl == default) - { - (streamUrl, _) = await streamUrlFactory(); - } - - // make a new one for later use - _ = Task.Run(async () => - { - (streamUrl, var expiry) = await streamUrlFactory(); - await CacheStreamUrlAsync(id, platform, streamUrl, expiry); - }); - - return streamUrl; - } - - public async Task CacheStreamUrlAsync( - string id, - MusicPlatform platform, - string url, - TimeSpan expiry) - => await _cache.AddAsync(GetStreamLinkKey(platform, id), url, expiry); - - // track data by id - private TypedKey GetTrackDataKey(MusicPlatform platform, string id) - => new($"music:track:{platform}:{id}"); - public async Task CacheTrackDataAsync(ICachableTrackData data) - => await _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data)); - - private CachableTrackData ToCachableTrackData(ICachableTrackData data) - => new CachableTrackData() - { - Id = data.Id, - Platform = data.Platform, - Thumbnail = data.Thumbnail, - Title = data.Title, - Url = data.Url, - }; - - public async Task GetCachedDataByIdAsync(string id, MusicPlatform platform) - => await _cache.GetOrDefaultAsync(GetTrackDataKey(platform, id)); - - - // track data by query - private TypedKey GetTrackDataQueryKey(MusicPlatform platform, string query) - => new($"music:track:{platform}:q:{query}"); - - public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data) - => await Task.WhenAll( - _cache.AddAsync(GetTrackDataQueryKey(data.Platform, query), ToCachableTrackData(data)).AsTask(), - _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data)).AsTask()); - - public async Task GetCachedDataByQueryAsync(string query, MusicPlatform platform) - => await _cache.GetOrDefaultAsync(GetTrackDataQueryKey(platform, query)); - - - // playlist track ids by playlist id - private TypedKey> GetPlaylistTracksCacheKey(string playlist, MusicPlatform platform) - => new($"music:playlist_tracks:{platform}:{playlist}"); - - public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids) - => await _cache.AddAsync(GetPlaylistTracksCacheKey(playlistId, platform), ids.ToList()); - - public async Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform) - { - var result = await _cache.GetAsync(GetPlaylistTracksCacheKey(playlistId, platform)); - if (result.TryGetValue(out var val)) - return val; - - return Array.Empty(); - } - - - // playlist id by query - private TypedKey GetPlaylistCacheKey(string query, MusicPlatform platform) - => new($"music:playlist_id:{platform}:{query}"); - - public async Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId) - => await _cache.AddAsync(GetPlaylistCacheKey(query, platform), playlistId); - - public async Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform) - => await _cache.GetOrDefaultAsync(GetPlaylistCacheKey(query, platform)); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Impl/VoiceProxy.cs b/src/Ellie.Bot.Modules.Music/_common/Impl/VoiceProxy.cs deleted file mode 100644 index 873b8d8..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Impl/VoiceProxy.cs +++ /dev/null @@ -1,102 +0,0 @@ -#nullable disable -using Ayu.Discord.Voice; -using Ayu.Discord.Voice.Models; - -namespace Ellie.Modules.Music; - -public sealed class VoiceProxy : IVoiceProxy -{ - public enum VoiceProxyState - { - Created, - Started, - Stopped - } - - private const int MAX_ERROR_COUNT = 20; - private const int DELAY_ON_ERROR_MILISECONDS = 200; - - public VoiceProxyState State - => gateway switch - { - { Started: true, Stopped: false } => VoiceProxyState.Started, - { Stopped: false } => VoiceProxyState.Created, - _ => VoiceProxyState.Stopped - }; - - - private VoiceGateway gateway; - - public VoiceProxy(VoiceGateway initial) - => gateway = initial; - - public bool SendPcmFrame(VoiceClient vc, Span data, int length) - { - try - { - var gw = gateway; - if (gw is null || gw.Stopped || !gw.Started) - return false; - - vc.SendPcmFrame(gw, data, 0, length); - return true; - } - catch (Exception) - { - return false; - } - } - - public async Task RunGatewayAction(Func action) - { - var errorCount = 0; - do - { - if (State == VoiceProxyState.Stopped) - break; - - try - { - var gw = gateway; - if (gw is null || !gw.ConnectingFinished.Task.IsCompleted) - { - ++errorCount; - await Task.Delay(DELAY_ON_ERROR_MILISECONDS); - Log.Debug("Gateway is not ready"); - continue; - } - - await action(gw); - errorCount = 0; - } - catch (Exception ex) - { - ++errorCount; - await Task.Delay(DELAY_ON_ERROR_MILISECONDS); - Log.Debug(ex, "Error performing proxy gateway action"); - } - } while (errorCount is > 0 and <= MAX_ERROR_COUNT); - - return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT; - } - - public void SetGateway(VoiceGateway newGateway) - => gateway = newGateway; - - public Task StartSpeakingAsync() - => RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone)); - - public Task StopSpeakingAsync() - => RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.None)); - - public async Task StartGateway() - => await gateway.Start(); - - public Task StopGateway() - { - if (gateway is { } gw) - return gw.StopAsync(); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Resolvers/LocalTrackResolver.cs b/src/Ellie.Bot.Modules.Music/_common/Resolvers/LocalTrackResolver.cs deleted file mode 100644 index e300e8f..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Resolvers/LocalTrackResolver.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Text; - -namespace Ellie.Modules.Music.Resolvers; - -public sealed class LocalTrackResolver : ILocalTrackResolver -{ - private static readonly HashSet _musicExtensions = new[] - { - ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX", - ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V" - }.ToHashSet(); - - public async Task ResolveByQueryAsync(string query) - { - if (!File.Exists(query)) - return null; - - var trackDuration = await Ffprobe.GetTrackDurationAsync(query); - return new SimpleTrackInfo(Path.GetFileNameWithoutExtension(query), - $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}", - "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png", - trackDuration, - MusicPlatform.Local, - $"\"{Path.GetFullPath(query)}\""); - } - - public async IAsyncEnumerable ResolveDirectoryAsync(string dirPath) - { - DirectoryInfo dir; - try - { - dir = new(dirPath); - } - catch (Exception ex) - { - Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath); - yield break; - } - - var files = dir.EnumerateFiles() - .Where(x => - { - if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) - && _musicExtensions.Contains(x.Extension.ToUpperInvariant())) - return true; - return false; - }) - .ToList(); - - var firstFile = files.FirstOrDefault()?.FullName; - if (firstFile is null) - yield break; - - var firstData = await ResolveByQueryAsync(firstFile); - if (firstData is not null) - yield return firstData; - - var fileChunks = files.Skip(1).Chunk(10); - foreach (var chunk in fileChunks) - { - var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll(); - - // nullable reference types being annoying - foreach (var p in part) - { - if (p is null) - continue; - - yield return p; - } - } - } -} - -public static class Ffprobe -{ - public static async Task GetTrackDurationAsync(string query) - { - query = query.Replace("\"", ""); - - try - { - using var p = Process.Start(new ProcessStartInfo - { - FileName = "ffprobe", - Arguments = - $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - CreateNoWindow = true - }); - - if (p is null) - return TimeSpan.Zero; - - var data = await p.StandardOutput.ReadToEndAsync(); - if (double.TryParse(data, out var seconds)) - return TimeSpan.FromSeconds(seconds); - - var errorData = await p.StandardError.ReadToEndAsync(); - if (!string.IsNullOrWhiteSpace(errorData)) - Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData); - - return TimeSpan.Zero; - } - catch (Win32Exception) - { - Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)"); - } - catch (Exception ex) - { - Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message); - } - - return TimeSpan.Zero; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Resolvers/RadioResolveStrategy.cs b/src/Ellie.Bot.Modules.Music/_common/Resolvers/RadioResolveStrategy.cs deleted file mode 100644 index 39c23ef..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Resolvers/RadioResolveStrategy.cs +++ /dev/null @@ -1,106 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace Ellie.Modules.Music.Resolvers; - -public class RadioResolver : IRadioResolver -{ - private readonly Regex _plsRegex = new(@"File1=(?.*?)\n", RegexOptions.Compiled); - private readonly Regex _m3URegex = new(@"(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); - private readonly Regex _asxRegex = new(@".*?)""", RegexOptions.Compiled); - private readonly Regex _xspfRegex = new(@"(?.*?)", RegexOptions.Compiled); - - public async Task ResolveByQueryAsync(string query) - { - if (IsRadioLink(query)) - query = await HandleStreamContainers(query); - - return new SimpleTrackInfo(query.TrimTo(50), - query, - "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png", - TimeSpan.MaxValue, - MusicPlatform.Radio, - query); - } - - public static bool IsRadioLink(string query) - => (query.StartsWith("http", StringComparison.InvariantCulture) - || query.StartsWith("ww", StringComparison.InvariantCulture)) - && (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf")); - - private async Task HandleStreamContainers(string query) - { - string file = null; - try - { - using var http = new HttpClient(); - file = await http.GetStringAsync(query); - } - catch - { - return query; - } - - if (query.Contains(".pls")) - { - try - { - var m = _plsRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning("Failed reading .pls:\n{PlsFile}", file); - return null; - } - } - - if (query.Contains(".m3u")) - { - try - { - var m = _m3URegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning("Failed reading .m3u:\n{M3uFile}", file); - return null; - } - } - - if (query.Contains(".asx")) - { - try - { - var m = _asxRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning("Failed reading .asx:\n{AsxFile}", file); - return null; - } - } - - if (query.Contains(".xspf")) - { - try - { - var m = _xspfRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning("Failed reading .xspf:\n{XspfFile}", file); - return null; - } - } - - return query; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Resolvers/SoundcloudResolver.cs b/src/Ellie.Bot.Modules.Music/_common/Resolvers/SoundcloudResolver.cs deleted file mode 100644 index 36664d4..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Resolvers/SoundcloudResolver.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Newtonsoft.Json.Linq; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; - -namespace Ellie.Modules.Music.Resolvers; - -public sealed class SoundcloudResolver : ISoundcloudResolver -{ - private readonly SoundCloudApiService _sc; - private readonly ITrackCacher _trackCacher; - private readonly IHttpClientFactory _httpFactory; - - public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory) - { - _sc = sc; - _trackCacher = trackCacher; - _httpFactory = httpFactory; - } - - public bool IsSoundCloudLink(string url) - => Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)"); - - public async IAsyncEnumerable ResolvePlaylistAsync(string playlist) - { - playlist = Uri.EscapeDataString(playlist); - - using var http = _httpFactory.CreateClient(); - var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}"); - var scvids = JObject.Parse(responseString)["tracks"]?.ToObject(); - if (scvids is null) - yield break; - - foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5)) - { - var cachableTracks = videosChunk.Select(VideoModelToCachedData).ToList(); - - await cachableTracks.Select(_trackCacher.CacheTrackDataAsync).WhenAll(); - foreach (var info in cachableTracks.Select(CachableDataToTrackInfo)) - yield return info; - } - } - - private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo) - => new CachableTrackData - { - Title = svideo.FullName, - Url = svideo.TrackLink, - Thumbnail = svideo.ArtworkUrl, - TotalDurationMs = svideo.Duration, - Id = svideo.Id.ToString(), - Platform = MusicPlatform.SoundCloud - }; - - private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData) - => new SimpleTrackInfo(trackData.Title, - trackData.Url, - trackData.Thumbnail, - trackData.Duration, - trackData.Platform, - GetStreamUrl(trackData.Id)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetStreamUrl(string trackId) - => $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0"; - - public async Task ResolveByQueryAsync(string query) - { - var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud); - if (cached is not null) - return CachableDataToTrackInfo(cached); - - var svideo = !IsSoundCloudLink(query) - ? await _sc.GetVideoByQueryAsync(query) - : await _sc.ResolveVideoAsync(query); - - if (svideo is null) - return null; - - var cachableData = VideoModelToCachedData(svideo); - await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData); - - return CachableDataToTrackInfo(cachableData); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Resolvers/TrackResolveProvider.cs b/src/Ellie.Bot.Modules.Music/_common/Resolvers/TrackResolveProvider.cs deleted file mode 100644 index 76b3981..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Resolvers/TrackResolveProvider.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Ellie.Modules.Music.Resolvers; - -public sealed class TrackResolveProvider : ITrackResolveProvider -{ - private readonly IYoutubeResolver _ytResolver; - private readonly ILocalTrackResolver _localResolver; - private readonly ISoundcloudResolver _soundcloudResolver; - private readonly IRadioResolver _radioResolver; - - public TrackResolveProvider( - IYoutubeResolver ytResolver, - ILocalTrackResolver localResolver, - ISoundcloudResolver soundcloudResolver, - IRadioResolver radioResolver) - { - _ytResolver = ytResolver; - _localResolver = localResolver; - _soundcloudResolver = soundcloudResolver; - _radioResolver = radioResolver; - } - - public Task QuerySongAsync(string query, MusicPlatform? forcePlatform) - { - switch (forcePlatform) - { - case MusicPlatform.Radio: - return _radioResolver.ResolveByQueryAsync(query); - case MusicPlatform.Youtube: - return _ytResolver.ResolveByQueryAsync(query); - case MusicPlatform.Local: - return _localResolver.ResolveByQueryAsync(query); - case MusicPlatform.SoundCloud: - return _soundcloudResolver.ResolveByQueryAsync(query); - case null: - var match = _ytResolver.YtVideoIdRegex.Match(query); - if (match.Success) - return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value); - else if (_soundcloudResolver.IsSoundCloudLink(query)) - return _soundcloudResolver.ResolveByQueryAsync(query); - else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile) - return _localResolver.ResolveByQueryAsync(uri.AbsolutePath); - else if (IsRadioLink(query)) - return _radioResolver.ResolveByQueryAsync(query); - else - return _ytResolver.ResolveByQueryAsync(query, false); - default: - Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform); - return Task.FromResult(null); - } - } - - public static bool IsRadioLink(string query) - => (query.StartsWith("http", StringComparison.InvariantCulture) - || query.StartsWith("ww", StringComparison.InvariantCulture)) - && (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf")); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/Resolvers/YtdlYoutubeResolver.cs b/src/Ellie.Bot.Modules.Music/_common/Resolvers/YtdlYoutubeResolver.cs deleted file mode 100644 index ecf7a02..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/Resolvers/YtdlYoutubeResolver.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; -using Ellie.Modules.Searches; - -namespace Ellie.Modules.Music; - -public sealed class YtdlYoutubeResolver : IYoutubeResolver -{ - private static readonly string[] _durationFormats = - { - "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" - }; - - private static readonly Regex _expiryRegex = new(@"(?:[\?\&]expire\=(?\d+))"); - - - private static readonly Regex _simplePlaylistRegex = new(@"&list=(?[\w\-]{12,})", RegexOptions.Compiled); - - public Regex YtVideoIdRegex { get; } = - new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", - RegexOptions.Compiled); - - private readonly ITrackCacher _trackCacher; - - private readonly YtdlOperation _ytdlPlaylistOperation; - private readonly YtdlOperation _ytdlIdOperation; - private readonly YtdlOperation _ytdlSearchOperation; - - private readonly IGoogleApiService _google; - - public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google, SearchesConfigService scs) - { - _trackCacher = trackCacher; - _google = google; - - - _ytdlPlaylistOperation = new("-4 " - + "--geo-bypass " - + "--encoding UTF8 " - + "-f bestaudio " - + "-e " - + "--get-url " - + "--get-id " - + "--get-thumbnail " - + "--get-duration " - + "--no-check-certificate " - + "-i " - + "--yes-playlist " - + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); - - _ytdlIdOperation = new("-4 " - + "--geo-bypass " - + "--encoding UTF8 " - + "-f bestaudio " - + "-e " - + "--get-url " - + "--get-id " - + "--get-thumbnail " - + "--get-duration " - + "--no-check-certificate " - + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); - - _ytdlSearchOperation = new("-4 " - + "--geo-bypass " - + "--encoding UTF8 " - + "-f bestaudio " - + "-e " - + "--get-url " - + "--get-id " - + "--get-thumbnail " - + "--get-duration " - + "--no-check-certificate " - + "--default-search " - + "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); - } - - private YtTrackData ResolveYtdlData(string ytdlOutputString) - { - if (string.IsNullOrWhiteSpace(ytdlOutputString)) - return default; - - var dataArray = ytdlOutputString.Trim().Split('\n'); - - if (dataArray.Length < 5) - { - Log.Information("Not enough data received: {YtdlData}", ytdlOutputString); - return default; - } - - if (!TimeSpan.TryParseExact(dataArray[4], _durationFormats, CultureInfo.InvariantCulture, out var time)) - time = TimeSpan.Zero; - - var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) ? dataArray[3].Trim() : string.Empty; - - return new(dataArray[0], dataArray[1], thumbnail, dataArray[2], time); - } - - private ITrackInfo DataToInfo(in YtTrackData trackData) - => new RemoteTrackInfo( - trackData.Id, - trackData.Title, - $"https://youtube.com/watch?v={trackData.Id}", - trackData.Thumbnail, - trackData.Duration, - MusicPlatform.Youtube, - CreateCacherFactory(trackData.Id)); - - private Func> CreateCacherFactory(string id) - => () => _trackCacher.GetOrCreateStreamLink(id, - MusicPlatform.Youtube, - async () => await ExtractNewStreamUrlAsync(id)); - - private static TimeSpan GetExpiry(string streamUrl) - { - var match = _expiryRegex.Match(streamUrl); - if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp)) - { - var realExpiry = timestamp.ToUnixTimestamp() - DateTime.UtcNow; - if (realExpiry > TimeSpan.FromMinutes(60)) - return realExpiry.Subtract(TimeSpan.FromMinutes(30)); - - return realExpiry; - } - - return TimeSpan.FromHours(1); - } - - private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id) - { - var data = await _ytdlIdOperation.GetDataAsync(id); - var trackInfo = ResolveYtdlData(data); - if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl)) - return default; - - return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!)); - } - - public async Task ResolveByIdAsync(string id) - { - id = id.Trim(); - - var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube); - if (cachedData is null) - { - Log.Information("Resolving youtube track by Id: {YoutubeId}", id); - - var data = await _ytdlIdOperation.GetDataAsync(id); - - var trackInfo = ResolveYtdlData(data); - if (string.IsNullOrWhiteSpace(trackInfo.Title)) - return default; - - var toReturn = DataToInfo(in trackInfo); - - await Task.WhenAll(_trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)), - CacheStreamUrlAsync(trackInfo)); - - return toReturn; - } - - return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration)); - } - - private Task CacheStreamUrlAsync(YtTrackData trackInfo) - => _trackCacher.CacheStreamUrlAsync(trackInfo.Id, - MusicPlatform.Youtube, - trackInfo.StreamUrl!, - GetExpiry(trackInfo.StreamUrl!)); - - public async IAsyncEnumerable ResolveTracksByPlaylistIdAsync(string playlistId) - { - Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId); - var count = 0; - - var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube); - if (ids.Count > 0) - { - foreach (var id in ids) - { - var trackInfo = await ResolveByIdAsync(id); - if (trackInfo is null) - continue; - - yield return trackInfo; - } - - yield break; - } - - var data = string.Empty; - var trackIds = new List(); - await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId)) - { - data += line; - - if (++count == 5) - { - var trackData = ResolveYtdlData(data); - data = string.Empty; - count = 0; - if (string.IsNullOrWhiteSpace(trackData.Id)) - continue; - - var info = DataToInfo(in trackData); - await Task.WhenAll(_trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)), - CacheStreamUrlAsync(trackData)); - - trackIds.Add(trackData.Id); - yield return info; - } - else - data += Environment.NewLine; - } - - await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds); - } - - public async IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query) - { - string? playlistId; - // try to match playlist id inside the query, if a playlist url has been queried - var match = _simplePlaylistRegex.Match(query); - if (match.Success) - { - // if it's a success, just return from that playlist using the id - playlistId = match.Groups["id"].ToString(); - await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) - yield return track; - - yield break; - } - - // if a query is a search term, try the cache - playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube); - if (playlistId is null) - { - // if it's not in the cache - // find playlist id by keyword using google api - try - { - var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query); - playlistId = playlistIds.FirstOrDefault(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error Getting playlist id via GoogleApi"); - } - - // if query is not a playlist url - // and query result is not in the cache - // and api returns no values - // it means invalid input has been used, - // or google api key is not provided - if (playlistId is null) - yield break; - } - - // cache the query -> playlist id for fast future lookup - await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId); - await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) - yield return track; - } - - public Task ResolveByQueryAsync(string query) - => ResolveByQueryAsync(query, true); - - public async Task ResolveByQueryAsync(string query, bool tryResolving) - { - if (tryResolving) - { - var match = YtVideoIdRegex.Match(query); - if (match.Success) - return await ResolveByIdAsync(match.Groups["id"].Value); - } - - Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query); - - var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube); - if (cachedData is null || string.IsNullOrWhiteSpace(cachedData.Title)) - { - var stringData = await _ytdlSearchOperation.GetDataAsync(query); - var trackData = ResolveYtdlData(stringData); - - var trackInfo = DataToInfo(trackData); - await Task.WhenAll(_trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)), - CacheStreamUrlAsync(trackData)); - return trackInfo; - } - - return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration)); - } - - private readonly struct YtTrackData - { - public readonly string Title; - public readonly string Id; - public readonly string Thumbnail; - public readonly string? StreamUrl; - public readonly TimeSpan Duration; - - public YtTrackData( - string title, - string id, - string thumbnail, - string? streamUrl, - TimeSpan duration) - { - Title = title.Trim(); - Id = id.Trim(); - Thumbnail = thumbnail; - StreamUrl = streamUrl; - Duration = duration; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlayerSettingsExtensions.cs b/src/Ellie.Bot.Modules.Music/_common/db/MusicPlayerSettingsExtensions.cs deleted file mode 100644 index 4e96eca..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlayerSettingsExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class MusicPlayerSettingsExtensions -{ - public static async Task ForGuildAsync(this DbSet settings, ulong guildId) - { - var toReturn = await settings.AsQueryable().FirstOrDefaultAsync(x => x.GuildId == guildId); - - if (toReturn is null) - { - var newSettings = new MusicPlayerSettings - { - GuildId = guildId, - PlayerRepeat = PlayerRepeatType.Queue - }; - - await settings.AddAsync(newSettings); - return newSettings; - } - - return toReturn; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylist.cs b/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylist.cs deleted file mode 100644 index 178c09e..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylist.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class MusicPlaylist : DbEntity -{ - public string Name { get; set; } - public string Author { get; set; } - public ulong AuthorId { get; set; } - public List Songs { get; set; } = new(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylistExtensions.cs b/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylistExtensions.cs deleted file mode 100644 index 30bdcb0..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/db/MusicPlaylistExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Db; - -public static class MusicPlaylistExtensions -{ - public static List GetPlaylistsOnPage(this DbSet playlists, int num) - { - if (num < 1) - throw new ArgumentOutOfRangeException(nameof(num)); - - return playlists.AsQueryable().Skip((num - 1) * 20).Take(20).Include(pl => pl.Songs).ToList(); - } - - public static MusicPlaylist GetWithSongs(this DbSet playlists, int id) - => playlists.Include(mpl => mpl.Songs).FirstOrDefault(mpl => mpl.Id == id); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Music/_common/db/MusicSettings.cs b/src/Ellie.Bot.Modules.Music/_common/db/MusicSettings.cs deleted file mode 100644 index 37d195a..0000000 --- a/src/Ellie.Bot.Modules.Music/_common/db/MusicSettings.cs +++ /dev/null @@ -1,61 +0,0 @@ -#nullable disable -namespace Ellie.Services.Database.Models; - -public class MusicPlayerSettings -{ - /// - /// Auto generated Id - /// - public int Id { get; set; } - - /// - /// Id of the guild - /// - public ulong GuildId { get; set; } - - /// - /// Queue repeat type - /// - public PlayerRepeatType PlayerRepeat { get; set; } = PlayerRepeatType.Queue; - - /// - /// Channel id the bot will always try to send track related messages to - /// - public ulong? MusicChannelId { get; set; } - - /// - /// Default volume player will be created with - /// - public int Volume { get; set; } = 100; - - /// - /// Whether the bot should auto disconnect from the voice channel once the queue is done - /// This only has effect if - /// - public bool AutoDisconnect { get; set; } - - /// - /// Selected quality preset for the music player - /// - public QualityPreset QualityPreset { get; set; } - - /// - /// Whether the bot will automatically queue related songs - /// - public bool AutoPlay { get; set; } -} - -public enum QualityPreset -{ - Highest, - High, - Medium, - Low -} - -public enum PlayerRepeatType -{ - None, - Track, - Queue -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Patronage/Patronage/Config/PatronageConfig.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Config/PatronageConfig.cs deleted file mode 100644 index 9249474..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Config/PatronageConfig.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Ellie.Common.Configs; - -namespace Ellie.Modules.Patronage; - -public class PatronageConfig : ConfigServiceBase -{ - public override string Name - => "patron"; - - private static readonly TypedKey _changeKey - = new("config.patron.updated"); - - private const string FILE_PATH = "data/patron.yml"; - - public PatronageConfig(IConfigSeria serializer, IPubSub pubSub) : base(FILE_PATH, serializer, pubSub, _changeKey) - { - AddParsedProp("enabled", - x => x.IsEnabled, - bool.TryParse, - ConfigPrinters.ToString); - - Migrate(); - } - - private void Migrate() - { - ModifyConfig(c => - { - if (c.Version == 1) - { - c.Version = 2; - c.IsEnabled = false; - } - }); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Patronage/Patronage/CurrencyRewardService.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/CurrencyRewardService.cs deleted file mode 100644 index 6b4c784..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/CurrencyRewardService.cs +++ /dev/null @@ -1,195 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Modules.Gambling.Services; -using Ellie.Modules.Patronage; -using Ellie.Services.Currency; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Utility; - -public sealed class CurrencyRewardService : IEService, IDisposable -{ - private readonly ICurrencyService _cs; - private readonly IPatronageService _ps; - private readonly DbService _db; - private readonly IEmbedBuilderService _eb; - private readonly GamblingConfigService _config; - private readonly DiscordSocketClient _client; - - public CurrencyRewardService( - ICurrencyService cs, - IPatronageService ps, - DbService db, - IEmbedBuilderService eb, - GamblingConfigService config, - DiscordSocketClient client) - { - _cs = cs; - _ps = ps; - _db = db; - _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 pledge was increased - if (oldPatron.Amount < newPatron.Amount) - { - var conf = _config.Data; - var newAmount = (long)(newPatron.Amount * conf.PatreonCurrencyPerCent); - - RewardedUser old; - await using (var ctx = _db.GetDbContext()) - { - old = await ctx.GetTable() - .Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId) - .FirstOrDefaultAsync(); - - if (old is null) - { - await OnNewPayment(newPatron); - return; - } - - // no action as the amount is the same or lower - if (old.AmountRewardedThisMonth >= newAmount) - return; - - var count = await ctx.GetTable() - .Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId) - .UpdateAsync(_ => new() - { - PlatformUserId = newPatron.UniquePlatformUserId, - UserId = newPatron.UserId, - // amount before bonuses - AmountRewardedThisMonth = newAmount, - LastReward = newPatron.PaidAt - }); - - // shouldn't ever happen - if (count == 0) - return; - } - - var oldAmount = old.AmountRewardedThisMonth; - - var realNewAmount = GetRealCurrencyReward( - (int)(newAmount / conf.PatreonCurrencyPerCent), - newAmount, - out var percentBonus); - - var realOldAmount = GetRealCurrencyReward( - (int)(oldAmount / conf.PatreonCurrencyPerCent), - oldAmount, - out _); - - var diff = realNewAmount - realOldAmount; - 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% - - await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update")); - - _ = SendMessageToUser(newPatron.UserId, - $"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!"); - } - } - - private long GetRealCurrencyReward(int pledgeCents, long modifiedAmount, out int percentBonus) - { - // needs at least 5$ to be eligible for a bonus - if (pledgeCents < 500) - { - percentBonus = 0; - return modifiedAmount; - } - - var dollarValue = pledgeCents / 100; - percentBonus = dollarValue switch - { - >= 100 => 100, - >= 50 => 50, - >= 20 => 20, - >= 10 => 10, - >= 5 => 5, - _ => 0 - }; - return (long)(modifiedAmount * (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.UniquePlatformUserId, - UserId = patron.UserId, - AmountRewardedThisMonth = amount, - LastReward = patron.PaidAt, - }, - old => new() - { - AmountRewardedThisMonth = amount, - UserId = patron.UserId, - LastReward = patron.PaidAt - }, - () => new() - { - PlatformUserId = patron.UniquePlatformUserId - }); - - 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() - .UpdateAsync(old => new() - { - AmountRewardedThisMonth = old.AmountRewardedThisMonth * 2 - }); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Patronage/Patronage/InsufficientTier.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/InsufficientTier.cs deleted file mode 100644 index 8d49522..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/InsufficientTier.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ellie.Db.Models; - -namespace Ellie.Modules.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/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonClient.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonClient.cs deleted file mode 100644 index ad1f4bb..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonClient.cs +++ /dev/null @@ -1,149 +0,0 @@ -#nullable disable -using OneOf; -using OneOf.Types; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Ellie.Modules.Patronage; - -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/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonCredentials.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonCredentials.cs deleted file mode 100644 index 9576612..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonCredentials.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Patronage; - -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/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonData.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonData.cs deleted file mode 100644 index 99c5247..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonData.cs +++ /dev/null @@ -1,134 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Patronage; - -public sealed class Attributes -{ - [JsonPropertyName("full_name")] - public string FullName { get; set; } - - [JsonPropertyName("is_follower")] - public bool IsFollower { get; set; } - - [JsonPropertyName("last_charge_date")] - public DateTime? LastChargeDate { get; set; } - - [JsonPropertyName("last_charge_status")] - public string LastChargeStatus { get; set; } - - [JsonPropertyName("lifetime_support_cents")] - public int LifetimeSupportCents { get; set; } - - [JsonPropertyName("currently_entitled_amount_cents")] - public int CurrentlyEntitledAmountCents { get; set; } - - [JsonPropertyName("patron_status")] - public string PatronStatus { get; set; } -} - -public sealed class Data -{ - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } -} - -public sealed class Address -{ - [JsonPropertyName("data")] - public Data Data { get; set; } -} - -// public sealed class CurrentlyEntitledTiers -// { -// [JsonPropertyName("data")] -// public List Data { get; set; } -// } - -// public sealed class Relationships -// { -// [JsonPropertyName("address")] -// public Address Address { get; set; } -// -// // [JsonPropertyName("currently_entitled_tiers")] -// // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; } -// } - -public sealed class PatreonMembersResponse -{ - [JsonPropertyName("data")] - public List Data { get; set; } - - [JsonPropertyName("included")] - public List Included { get; set; } - - [JsonPropertyName("links")] - public PatreonLinks Links { get; set; } -} - -public sealed class PatreonLinks -{ - [JsonPropertyName("next")] - public string Next { get; set; } -} - -public sealed class PatreonUser -{ - [JsonPropertyName("attributes")] - public PatreonUserAttributes Attributes { get; set; } - - [JsonPropertyName("id")] - public string Id { get; set; } - // public string Type { get; set; } -} - -public sealed class PatreonUserAttributes -{ - [JsonPropertyName("social_connections")] - public PatreonSocials SocialConnections { get; set; } -} - -public sealed class PatreonSocials -{ - [JsonPropertyName("discord")] - public DiscordSocial Discord { get; set; } -} - -public sealed class DiscordSocial -{ - [JsonPropertyName("user_id")] - public string UserId { get; set; } -} - -public sealed class PatreonMember -{ - [JsonPropertyName("attributes")] - public Attributes Attributes { get; set; } - - [JsonPropertyName("relationships")] - public Relationships Relationships { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } -} - -public sealed class Relationships -{ - [JsonPropertyName("user")] - public PatreonRelationshipUser User { get; set; } -} - -public sealed class PatreonRelationshipUser -{ - [JsonPropertyName("data")] - public PatreonUserData Data { get; set; } -} - -public sealed class PatreonUserData -{ - [JsonPropertyName("id")] - public string Id { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonMemberData.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonMemberData.cs deleted file mode 100644 index 60471d7..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonMemberData.cs +++ /dev/null @@ -1,33 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Patronage; - -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/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonRefreshData.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonRefreshData.cs deleted file mode 100644 index 9a874fb..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonRefreshData.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Patronage; - -public sealed class PatreonRefreshData -{ - [JsonPropertyName("access_token")] - public string AccessToken { get; set; } - - [JsonPropertyName("refresh_token")] - public string RefreshToken { get; set; } - - [JsonPropertyName("expires_in")] - public long ExpiresIn { get; set; } - - [JsonPropertyName("scope")] - public string Scope { get; set; } - - [JsonPropertyName("token_type")] - public string TokenType { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonSubscriptionHandler.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonSubscriptionHandler.cs deleted file mode 100644 index d34f18f..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/Patreon/PatreonSubscriptionHandler.cs +++ /dev/null @@ -1,79 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Patronage; - -/// -/// Service tasked with handling pledges on patreon -/// -public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService -{ - 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/Ellie.Bot.Modules.Patronage/Patronage/PatronageCommands.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/PatronageCommands.cs deleted file mode 100644 index 2527350..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/PatronageCommands.cs +++ /dev/null @@ -1,148 +0,0 @@ -namespace Ellie.Modules.Patronage; - -[OnlyPublicBot] -public partial class Patronage : EllieModule -{ - private readonly PatronageService _service; - private readonly PatronageConfig _pConf; - - public Patronage(PatronageService service, PatronageConfig pConf) - { - _service = service; - _pConf = pConf; - } - - [Cmd] - [Priority(2)] - public Task Patron() - => InternalPatron(ctx.User); - - [Cmd] - [Priority(0)] - [OwnerOnly] - public Task Patron(IUser user) - => InternalPatron(user); - - [Cmd] - [Priority(0)] - [OwnerOnly] - public async 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 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/Ellie.Bot.Modules.Patronage/Patronage/PatronageService.cs b/src/Ellie.Bot.Modules.Patronage/Patronage/PatronageService.cs deleted file mode 100644 index 98dfeae..0000000 --- a/src/Ellie.Bot.Modules.Patronage/Patronage/PatronageService.cs +++ /dev/null @@ -1,837 +0,0 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db.Models; -using OneOf; -using OneOf.Types; -using CommandInfo = Discord.Commands.CommandInfo; - -namespace Ellie.Modules.Patronage; - -/// -public sealed class PatronageService - : IPatronageService, - IReadyExecutor, - IExecPreCommand, - IEService -{ - 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 static readonly TypedKey _quotaKey - = new($"quota:last_hourly_reset"); - - private readonly IBotCache _cache; - private readonly IBotCredsProvider _creds; - - public PatronageService( - PatronageConfig pConf, - DbService db, - DiscordSocketClient client, - ISubscriptionHandler subsHandler, - IEmbedBuilderService eb, - IBotCache cache, - IBotCredsProvider creds) - { - _pConf = pConf; - _db = db; - _client = client; - _subsHandler = subsHandler; - _eb = eb; - _cache = cache; - _creds = creds; - } - - 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 result = await _cache.GetAsync(_quotaKey); - if (result.TryGetValue(out var lastVal) && lastVal != default) - { - lastRun = DateTime.FromBinary(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, - }); - } - 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 _cache.AddAsync(_quotaKey, 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 || dbPatron.LastCharge.Year < lastChargeUtc.Year) - { - // 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) - { - var cents = subscriber.Cents; - // the user updated the pledge or changed the connected discord account - await ctx.GetTable() - .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId) - .UpdateAsync(old => new() - { - UserId = subscriber.UserId, - AmountCents = cents, - LastCharge = lastChargeUtc, - ValidThru = old.ValidThru, - }); - - var newPatron = dbPatron.Clone(); - newPatron.AmountCents = cents; - newPatron.UserId = subscriber.UserId; - - // idk what's going on but UpdateWithOutputAsync doesn't work properly here - // nor does firstordefault after update. I'm not seeing something obvious - await OnPatronUpdated( - PatronUserToPatron(dbPatron), - PatronUserToPatron(newPatron)); - } - } - } - catch (Exception ex) - { - Log.Error(ex, - "Unexpected error occured while processing rewards for patron {UserId}", - subscriber.UserId); - } - } - - var expiredDate = DateTime.MinValue; - foreach (var patron in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Refunded)) - { - // if the subscription is refunded, Disable user's valid thru - var changedCount = await ctx.GetTable() - .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId - && x.ValidThru != expiredDate) - .UpdateAsync(old => new() - { - ValidThru = expiredDate - }); - - if (changedCount == 0) - continue; - - var updated = await ctx.GetTable() - .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId) - .FirstAsync(); - - await OnPatronRefunded(PatronUserToPatron(updated)); - } - } - - 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.GetCreds().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>( - _ => new Success(), - 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, - }; - } - - 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 = defaultValue, - 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 = 0, - IsPatronLimit = true, - }; - - 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() - { - UniquePlatformUserId = 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 - { - >= 10_000 => PatronTier.C, - >= 5000 => PatronTier.L, - >= 2000 => PatronTier.XX, - >= 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.UniquePlatformUserId}"); - - 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; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/Blacklist/BlacklistCommands.cs b/src/Ellie.Bot.Modules.Permissions/Blacklist/BlacklistCommands.cs deleted file mode 100644 index 6c1774b..0000000 --- a/src/Ellie.Bot.Modules.Permissions/Blacklist/BlacklistCommands.cs +++ /dev/null @@ -1,143 +0,0 @@ -#nullable disable -using Ellie.Modules.Permissions.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions -{ - [Group] - public partial class BlacklistCommands : EllieModule - { - private readonly DiscordSocketClient _client; - - public BlacklistCommands(DiscordSocketClient client) - => _client = client; - - private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0) - { - if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); - - var list = _service.GetBlacklist(); - var items = await list.Where(x => x.Type == type) - .Select(async i => - { - try - { - return i.Type switch - { - BlacklistType.Channel => Format.Code(i.ItemId.ToString()) - + " " - + (_client.GetChannel(i.ItemId)?.ToString() - ?? ""), - BlacklistType.User => Format.Code(i.ItemId.ToString()) - + " " - + ((await _client.Rest.GetUserAsync(i.ItemId)) - ?.ToString() - ?? ""), - BlacklistType.Server => Format.Code(i.ItemId.ToString()) - + " " - + (_client.GetGuild(i.ItemId)?.ToString() ?? ""), - _ => Format.Code(i.ItemId.ToString()) - }; - } - catch - { - Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]", - i.Type, - i.ItemId); - return Format.Code(i.ItemId.ToString()); - } - }) - .WhenAll(); - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var pageItems = items.Skip(10 * curPage).Take(10).ToList(); - - if (pageItems.Count == 0) - return _eb.Create().WithOkColor().WithTitle(title).WithDescription(GetText(strs.empty_page)); - - return _eb.Create().WithTitle(title).WithDescription(pageItems.Join('\n')).WithOkColor(); - }, - items.Length, - 10); - } - - [Cmd] - [OwnerOnly] - public Task UserBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_users), BlacklistType.User, page); - } - - [Cmd] - [OwnerOnly] - public Task ChannelBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_channels), BlacklistType.Channel, page); - } - - [Cmd] - [OwnerOnly] - public Task ServerBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_servers), BlacklistType.Server, page); - } - - [Cmd] - [OwnerOnly] - public Task UserBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.User); - - [Cmd] - [OwnerOnly] - public Task UserBlacklist(AddRemove action, IUser usr) - => Blacklist(action, usr.Id, BlacklistType.User); - - [Cmd] - [OwnerOnly] - public Task ChannelBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.Channel); - - [Cmd] - [OwnerOnly] - public Task ServerBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.Server); - - [Cmd] - [OwnerOnly] - public Task ServerBlacklist(AddRemove action, IGuild guild) - => Blacklist(action, guild.Id, BlacklistType.Server); - - private async Task Blacklist(AddRemove action, ulong id, BlacklistType type) - { - if (action == AddRemove.Add) - await _service.Blacklist(type, id); - else - await _service.UnBlacklist(type, id); - - if (action == AddRemove.Add) - { - await ReplyConfirmLocalizedAsync(strs.blacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.unblacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs b/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs deleted file mode 100644 index 13c5f5c..0000000 --- a/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using static Ellie.Common.TypeReaders.TypeReaderResult; - -namespace Ellie.Modules.Permissions; - -public class CleverbotResponseCmdCdTypeReader : EllieTypeReader -{ - 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/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdService.cs b/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdService.cs deleted file mode 100644 index 5708448..0000000 --- a/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdService.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; - -namespace Ellie.Modules.Permissions.Services; - -public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, IEService -{ - private readonly DbService _db; - private readonly ConcurrentDictionary> _settings = new(); - - private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary> _activeCooldowns = - new(); - - public int Priority => 0; - - public CmdCdService(IBot bot, DbService db) - { - _db = db; - _settings = bot - .AllGuildConfigs - .ToDictionary(x => x.GuildId, x => x.CommandCooldowns - .DistinctBy(x => x.CommandName.ToLowerInvariant()) - .ToDictionary(c => c.CommandName, c => c.Seconds) - .ToConcurrent()) - .ToConcurrent(); - } - - public Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command) - => TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant()); - - public Task TryBlock(IGuild? guild, IUser user, string commandName) - { - if (guild is null) - return Task.FromResult(false); - - if (!_settings.TryGetValue(guild.Id, out var cooldownSettings)) - return Task.FromResult(false); - - if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds)) - return Task.FromResult(false); - - var cooldowns = _activeCooldowns.GetOrAdd( - (guild.Id, commandName), - static _ => new()); - - // if user is not already on cooldown, add - if (cooldowns.TryAdd(user.Id, DateTime.UtcNow)) - { - return Task.FromResult(false); - } - - // if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did - // - just update - if (cooldowns.TryGetValue(user.Id, out var oldValue)) - { - var diff = DateTime.UtcNow - oldValue; - if (diff.TotalSeconds > cdSeconds) - { - if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue)) - return Task.FromResult(false); - } - } - - return Task.FromResult(true); - } - - public async Task OnReadyAsync() - { - using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - - while (await timer.WaitForNextTickAsync()) - { - // once per hour delete expired entries - foreach (var ((guildId, commandName), dict) in _activeCooldowns) - { - // if this pair no longer has associated config, that means it has been removed. - // remove all cooldowns - if (!_settings.TryGetValue(guildId, out var inner) - || !inner.TryGetValue(commandName, out var cdSeconds)) - { - _activeCooldowns.Remove((guildId, commandName), out _); - continue; - } - - Cleanup(dict, cdSeconds); - } - } - } - - private void Cleanup(ConcurrentDictionary dict, int cdSeconds) - { - var now = DateTime.UtcNow; - foreach (var (key, _) in dict.Where(x => (now - x.Value).TotalSeconds > cdSeconds).ToArray()) - { - dict.TryRemove(key, out _); - } - } - - public void ClearCooldowns(ulong guildId, string cmdName) - { - if (_settings.TryGetValue(guildId, out var dict)) - dict.TryRemove(cmdName, out _); - - _activeCooldowns.TryRemove((guildId, cmdName), out _); - - using var ctx = _db.GetDbContext(); - var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns)); - gc.CommandCooldowns.RemoveWhere(x => x.CommandName == cmdName); - ctx.SaveChanges(); - } - - public void AddCooldown(ulong guildId, string name, int secs) - { - if (secs <= 0) - throw new ArgumentOutOfRangeException(nameof(secs)); - - var sett = _settings.GetOrAdd(guildId, static _ => new()); - sett[name] = secs; - - // force cleanup - if (_activeCooldowns.TryGetValue((guildId, name), out var dict)) - Cleanup(dict, secs); - - using var ctx = _db.GetDbContext(); - var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns)); - gc.CommandCooldowns.RemoveWhere(x => x.CommandName == name); - gc.CommandCooldowns.Add(new() - { - Seconds = secs, - CommandName = name - }); - ctx.SaveChanges(); - } - - public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId) - { - if (!_settings.TryGetValue(guildId, out var dict)) - return Array.Empty<(string, int)>(); - - return dict.Select(x => (x.Key, x.Value)).ToArray(); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdsCommands.cs b/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdsCommands.cs deleted file mode 100644 index 6f3e621..0000000 --- a/src/Ellie.Bot.Modules.Permissions/CommandCooldown/CmdCdsCommands.cs +++ /dev/null @@ -1,105 +0,0 @@ -#nullable disable -using Humanizer.Localisation; -using Microsoft.EntityFrameworkCore; -using Ellie.Common.TypeReaders; -using Ellie.Db; -using Ellie.Modules.Permissions.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions -{ - [Group] - public partial class CmdCdsCommands : EllieModule - { - private readonly DbService _db; - private readonly CmdCdService _service; - - public CmdCdsCommands(CmdCdService service, DbService db) - { - _service = service; - _db = db; - } - - private async Task CmdCooldownInternal(string cmdName, int secs) - { - var channel = (ITextChannel)ctx.Channel; - if (secs is < 0 or > 3600) - { - await ReplyErrorLocalizedAsync(strs.invalid_second_param_between(0, 3600)); - return; - } - - var name = cmdName.ToLowerInvariant(); - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns)); - - var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name); - if (toDelete is not null) - uow.Set().Remove(toDelete); - if (secs != 0) - { - var cc = new CommandCooldown - { - CommandName = name, - Seconds = secs - }; - config.CommandCooldowns.Add(cc); - _service.AddCooldown(channel.Guild.Id, name, secs); - } - - await uow.SaveChangesAsync(); - } - - if (secs == 0) - { - _service.ClearCooldowns(ctx.Guild.Id, cmdName); - await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name))); - } - else - await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task CmdCooldown(CleverBotResponseStr command, int secs) - => CmdCooldownInternal(CleverBotResponseStr.CLEVERBOT_RESPONSE, secs); - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task CmdCooldown(CommandOrExprInfo command, int secs) - => CmdCooldownInternal(command.Name, secs); - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AllCmdCooldowns(int page = 1) - { - if (--page < 0) - return; - - var channel = (ITextChannel)ctx.Channel; - var localSet = _service.GetCommandCooldowns(ctx.Guild.Id); - - if (!localSet.Any()) - await ReplyConfirmLocalizedAsync(strs.cmdcd_none); - else - { - await ctx.SendPaginatedConfirmAsync(page, curPage => - { - var items = localSet.Skip(curPage * 15) - .Take(15) - .Select(x => $"{Format.Code(x.CommandName)}: {x.Seconds.Seconds().Humanize(maxUnit: TimeUnit.Second, culture: Culture)}"); - - return _eb.Create(ctx) - .WithOkColor() - .WithDescription(items.Join("\n")); - - }, localSet.Count, 15); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/Filter/FilterCommands.cs b/src/Ellie.Bot.Modules.Permissions/Filter/FilterCommands.cs deleted file mode 100644 index b9a1d8f..0000000 --- a/src/Ellie.Bot.Modules.Permissions/Filter/FilterCommands.cs +++ /dev/null @@ -1,324 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Modules.Permissions.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions -{ - [Group] - public partial class FilterCommands : EllieModule - { - private readonly DbService _db; - - public FilterCommands(DbService db) - => _db = db; - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task FwClear() - { - _service.ClearFilteredWords(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.fw_cleared); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task FilterList() - { - var embed = _eb.Create(ctx) - .WithOkColor() - .WithTitle("Server filter settings"); - - var config = await _service.GetFilterSettings(ctx.Guild.Id); - - string GetEnabledEmoji(bool value) - => value ? "\\🟢" : "\\🔴"; - - async Task GetChannelListAsync(IReadOnlyCollection channels) - { - var toReturn = (await channels - .Select(async cid => - { - var ch = await ctx.Guild.GetChannelAsync(cid); - return ch is null - ? $"{cid} *missing*" - : $"<#{cid}>"; - }) - .WhenAll()) - .Join('\n'); - - if (string.IsNullOrWhiteSpace(toReturn)) - return GetText(strs.no_channel_found); - - return toReturn; - } - - embed.AddField($"{GetEnabledEmoji(config.FilterLinksEnabled)} Filter Links", - await GetChannelListAsync(config.FilterLinksChannels)); - - embed.AddField($"{GetEnabledEmoji(config.FilterInvitesEnabled)} Filter Invites", - await GetChannelListAsync(config.FilterInvitesChannels)); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterInv() - { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterInvites = !config.FilterInvites; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.InviteFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_server_on); - } - else - { - _service.InviteFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_server_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterInv() - { - var channel = (ITextChannel)ctx.Channel; - - FilterChannelId removed; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, - set => set.Include(gc => gc.FilterInvitesChannelIds)); - var match = new FilterChannelId - { - ChannelId = channel.Id - }; - removed = config.FilterInvitesChannelIds.FirstOrDefault(fc => fc.Equals(match)); - - if (removed is null) - config.FilterInvitesChannelIds.Add(match); - else - uow.Remove(removed); - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.InviteFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_on); - } - else - { - _service.InviteFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterLin() - { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterLinks = !config.FilterLinks; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.LinkFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_server_on); - } - else - { - _service.LinkFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_server_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterLin() - { - var channel = (ITextChannel)ctx.Channel; - - FilterLinksChannelId removed; - await using (var uow = _db.GetDbContext()) - { - var config = - uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterLinksChannelIds)); - var match = new FilterLinksChannelId - { - ChannelId = channel.Id - }; - removed = config.FilterLinksChannelIds.FirstOrDefault(fc => fc.Equals(match)); - - if (removed is null) - config.FilterLinksChannelIds.Add(match); - else - uow.Remove(removed); - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.LinkFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_channel_on); - } - else - { - _service.LinkFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_channel_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterWords() - { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterWords = !config.FilterWords; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.WordFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_server_on); - } - else - { - _service.WordFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_server_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterWords() - { - var channel = (ITextChannel)ctx.Channel; - - FilterWordsChannelId removed; - await using (var uow = _db.GetDbContext()) - { - var config = - uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterWordsChannelIds)); - - var match = new FilterWordsChannelId - { - ChannelId = channel.Id - }; - removed = config.FilterWordsChannelIds.FirstOrDefault(fc => fc.Equals(match)); - if (removed is null) - config.FilterWordsChannelIds.Add(match); - else - uow.Remove(removed); - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.WordFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_channel_on); - } - else - { - _service.WordFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_channel_off); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task FilterWord([Leftover] string word) - { - var channel = (ITextChannel)ctx.Channel; - - word = word?.Trim().ToLowerInvariant(); - - if (string.IsNullOrWhiteSpace(word)) - return; - - FilteredWord removed; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilteredWords)); - - removed = config.FilteredWords.FirstOrDefault(fw => fw.Word.Trim().ToLowerInvariant() == word); - - if (removed is null) - { - config.FilteredWords.Add(new() - { - Word = word - }); - } - else - uow.Remove(removed); - - await uow.SaveChangesAsync(); - } - - var filteredWords = - _service.ServerFilteredWords.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - - if (removed is null) - { - filteredWords.Add(word); - await ReplyConfirmLocalizedAsync(strs.filter_word_add(Format.Code(word))); - } - else - { - filteredWords.TryRemove(word); - await ReplyConfirmLocalizedAsync(strs.filter_word_remove(Format.Code(word))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task LstFilterWords(int page = 1) - { - page--; - if (page < 0) - return; - - var channel = (ITextChannel)ctx.Channel; - - _service.ServerFilteredWords.TryGetValue(channel.Guild.Id, out var fwHash); - - var fws = fwHash.ToArray(); - - await ctx.SendPaginatedConfirmAsync(page, - curPage => _eb.Create() - .WithTitle(GetText(strs.filter_word_list)) - .WithDescription(string.Join("\n", fws.Skip(curPage * 10).Take(10))) - .WithOkColor(), - fws.Length, - 10); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/Filter/FilterService.cs b/src/Ellie.Bot.Modules.Permissions/Filter/FilterService.cs deleted file mode 100644 index d7125a3..0000000 --- a/src/Ellie.Bot.Modules.Permissions/Filter/FilterService.cs +++ /dev/null @@ -1,241 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions.Services; - -public sealed class FilterService : IExecOnMessage -{ - public ConcurrentHashSet InviteFilteringChannels { get; } - public ConcurrentHashSet InviteFilteringServers { get; } - - //serverid, filteredwords - public ConcurrentDictionary> ServerFilteredWords { get; } - - public ConcurrentHashSet WordFilteringChannels { get; } - public ConcurrentHashSet WordFilteringServers { get; } - - public ConcurrentHashSet LinkFilteringChannels { get; } - public ConcurrentHashSet LinkFilteringServers { get; } - - public int Priority - => int.MaxValue - 1; - - private readonly DbService _db; - - public FilterService(DiscordSocketClient client, DbService db) - { - _db = db; - - using (var uow = db.GetDbContext()) - { - var ids = client.GetGuildIds(); - var configs = uow.Set() - .AsQueryable() - .Include(x => x.FilteredWords) - .Include(x => x.FilterLinksChannelIds) - .Include(x => x.FilterWordsChannelIds) - .Include(x => x.FilterInvitesChannelIds) - .Where(gc => ids.Contains(gc.GuildId)) - .ToList(); - - InviteFilteringServers = new(configs.Where(gc => gc.FilterInvites).Select(gc => gc.GuildId)); - InviteFilteringChannels = - new(configs.SelectMany(gc => gc.FilterInvitesChannelIds.Select(fci => fci.ChannelId))); - - LinkFilteringServers = new(configs.Where(gc => gc.FilterLinks).Select(gc => gc.GuildId)); - LinkFilteringChannels = - new(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId))); - - var dict = configs.ToDictionary(gc => gc.GuildId, - gc => new ConcurrentHashSet(gc.FilteredWords.Select(fw => fw.Word).Distinct())); - - ServerFilteredWords = new(dict); - - var serverFiltering = configs.Where(gc => gc.FilterWords); - WordFilteringServers = new(serverFiltering.Select(gc => gc.GuildId)); - WordFilteringChannels = - new(configs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId))); - } - - client.MessageUpdated += (oldData, newMsg, channel) => - { - _ = Task.Run(() => - { - var guild = (channel as ITextChannel)?.Guild; - - if (guild is null || newMsg is not IUserMessage usrMsg) - return Task.CompletedTask; - - return ExecOnMessageAsync(guild, usrMsg); - }); - return Task.CompletedTask; - }; - } - - public ConcurrentHashSet FilteredWordsForChannel(ulong channelId, ulong guildId) - { - var words = new ConcurrentHashSet(); - if (WordFilteringChannels.Contains(channelId)) - ServerFilteredWords.TryGetValue(guildId, out words); - return words; - } - - public void ClearFilteredWords(ulong guildId) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.FilteredWords).Include(x => x.FilterWordsChannelIds)); - - WordFilteringServers.TryRemove(guildId); - ServerFilteredWords.TryRemove(guildId, out _); - - foreach (var c in gc.FilterWordsChannelIds) - WordFilteringChannels.TryRemove(c.ChannelId); - - gc.FilterWords = false; - gc.FilteredWords.Clear(); - gc.FilterWordsChannelIds.Clear(); - - uow.SaveChanges(); - } - - public ConcurrentHashSet FilteredWordsForServer(ulong guildId) - { - var words = new ConcurrentHashSet(); - if (WordFilteringServers.Contains(guildId)) - ServerFilteredWords.TryGetValue(guildId, out words); - return words; - } - - public async Task ExecOnMessageAsync(IGuild guild, IUserMessage msg) - { - if (msg.Author is not IGuildUser gu || gu.GuildPermissions.Administrator) - return false; - - var results = await Task.WhenAll(FilterInvites(guild, msg), FilterWords(guild, msg), FilterLinks(guild, msg)); - - return results.Any(x => x); - } - - private async Task FilterWords(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - var filteredChannelWords = - FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet(); - var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet(); - var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); - if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) - { - foreach (var word in wordsInMessage) - { - if (filteredChannelWords.Contains(word) || filteredServerWords.Contains(word)) - { - Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync(); - } - catch (HttpException ex) - { - Log.Warning(ex, - "I do not have permission to filter words in channel with id {Id}", - usrMsg.Channel.Id); - } - - return true; - } - } - } - - return false; - } - - private async Task FilterInvites(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id)) - && usrMsg.Content.IsDiscordInvite()) - { - Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync(); - return true; - } - catch (HttpException ex) - { - Log.Warning(ex, - "I do not have permission to filter invites in channel with id {Id}", - usrMsg.Channel.Id); - return true; - } - } - - return false; - } - - private async Task FilterLinks(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) || LinkFilteringServers.Contains(guild.Id)) - && usrMsg.Content.TryGetUrlPath(out _)) - { - Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync(); - return true; - } - catch (HttpException ex) - { - Log.Warning(ex, "I do not have permission to filter links in channel with id {Id}", usrMsg.Channel.Id); - return true; - } - } - - return false; - } - - public async Task GetFilterSettings(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set - .Include(x => x.FilterInvitesChannelIds) - .Include(x => x.FilterLinksChannelIds)); - - return new() - { - FilterInvitesChannels = gc.FilterInvitesChannelIds.Map(x => x.ChannelId), - FilterLinksChannels = gc.FilterLinksChannelIds.Map(x => x.ChannelId), - FilterInvitesEnabled = gc.FilterInvites, - FilterLinksEnabled = gc.FilterLinks, - }; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/Filter/ServerFilterSettings.cs b/src/Ellie.Bot.Modules.Permissions/Filter/ServerFilterSettings.cs deleted file mode 100644 index 9135426..0000000 --- a/src/Ellie.Bot.Modules.Permissions/Filter/ServerFilterSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Permissions.Services; - -public readonly struct ServerFilterSettings -{ - public bool FilterInvitesEnabled { get; init; } - public bool FilterLinksEnabled { get; init; } - public IReadOnlyCollection FilterInvitesChannels { get; init; } - public IReadOnlyCollection FilterLinksChannels { get; init; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionCommands.cs b/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionCommands.cs deleted file mode 100644 index 714153e..0000000 --- a/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionCommands.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Modules.Permissions.Services; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions -{ - [Group] - public partial class GlobalPermissionCommands : EllieModule - { - private readonly GlobalPermissionService _service; - private readonly DbService _db; - - public GlobalPermissionCommands(GlobalPermissionService service, DbService db) - { - _service = service; - _db = db; - } - - [Cmd] - [OwnerOnly] - public async Task GlobalPermList() - { - var blockedModule = _service.BlockedModules; - var blockedCommands = _service.BlockedCommands; - if (!blockedModule.Any() && !blockedCommands.Any()) - { - await ReplyErrorLocalizedAsync(strs.lgp_none); - return; - } - - var embed = _eb.Create().WithOkColor(); - - if (blockedModule.Any()) - embed.AddField(GetText(strs.blocked_modules), string.Join("\n", _service.BlockedModules)); - - if (blockedCommands.Any()) - embed.AddField(GetText(strs.blocked_commands), string.Join("\n", _service.BlockedCommands)); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [OwnerOnly] - public async Task GlobalModule(ModuleOrCrInfo module) - { - var moduleName = module.Name.ToLowerInvariant(); - - var added = _service.ToggleModule(moduleName); - - if (added) - { - await ReplyConfirmLocalizedAsync(strs.gmod_add(Format.Bold(module.Name))); - return; - } - - await ReplyConfirmLocalizedAsync(strs.gmod_remove(Format.Bold(module.Name))); - } - - [Cmd] - [OwnerOnly] - public async Task GlobalCommand(CommandOrExprInfo cmd) - { - var commandName = cmd.Name.ToLowerInvariant(); - var added = _service.ToggleCommand(commandName); - - if (added) - { - await ReplyConfirmLocalizedAsync(strs.gcmd_add(Format.Bold(cmd.Name))); - return; - } - - await ReplyConfirmLocalizedAsync(strs.gcmd_remove(Format.Bold(cmd.Name))); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionService.cs b/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionService.cs deleted file mode 100644 index e1d7d0e..0000000 --- a/src/Ellie.Bot.Modules.Permissions/GlobalPermissions/GlobalPermissionService.cs +++ /dev/null @@ -1,92 +0,0 @@ -#nullable disable -using Ellie.Common.ModuleBehaviors; - -namespace Ellie.Modules.Permissions.Services; - -public class GlobalPermissionService : IExecPreCommand, IEService -{ - public int Priority { get; } = 0; - - public HashSet BlockedCommands - => _bss.Data.Blocked.Commands; - - public HashSet BlockedModules - => _bss.Data.Blocked.Modules; - - private readonly BotConfigService _bss; - - public GlobalPermissionService(BotConfigService bss) - => _bss = bss; - - - public Task ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command) - { - var settings = _bss.Data; - var commandName = command.Name.ToLowerInvariant(); - - if (commandName != "resetglobalperms" - && (settings.Blocked.Commands.Contains(commandName) - || settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))) - return Task.FromResult(true); - - return Task.FromResult(false); - } - - /// - /// Toggles module blacklist - /// - /// Lowercase module name - /// Whether the module is added - public bool ToggleModule(string moduleName) - { - var added = false; - _bss.ModifyConfig(bs => - { - if (bs.Blocked.Modules.Add(moduleName)) - added = true; - else - { - bs.Blocked.Modules.Remove(moduleName); - added = false; - } - }); - - return added; - } - - /// - /// Toggles command blacklist - /// - /// Lowercase command name - /// Whether the command is added - public bool ToggleCommand(string commandName) - { - var added = false; - _bss.ModifyConfig(bs => - { - if (bs.Blocked.Commands.Add(commandName)) - added = true; - else - { - bs.Blocked.Commands.Remove(commandName); - added = false; - } - }); - - return added; - } - - /// - /// Resets all global permissions - /// - public Task Reset() - { - _bss.ModifyConfig(bs => - { - bs.Blocked.Commands.Clear(); - bs.Blocked.Modules.Clear(); - }); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/PermissionCache.cs b/src/Ellie.Bot.Modules.Permissions/PermissionCache.cs deleted file mode 100644 index c3686ce..0000000 --- a/src/Ellie.Bot.Modules.Permissions/PermissionCache.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions.Common; - -public class PermissionCache -{ - public string PermRole { get; set; } - public bool Verbose { get; set; } = true; - public PermissionsCollection Permissions { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/PermissionExtensions.cs b/src/Ellie.Bot.Modules.Permissions/PermissionExtensions.cs deleted file mode 100644 index 4092eab..0000000 --- a/src/Ellie.Bot.Modules.Permissions/PermissionExtensions.cs +++ /dev/null @@ -1,132 +0,0 @@ -#nullable disable -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions.Common; - -public static class PermissionExtensions -{ - public static bool CheckPermissions( - this IEnumerable permsEnumerable, - IUser user, - IMessageChannel message, - string commandName, - string moduleName, - out int permIndex) - { - var perms = permsEnumerable as List ?? permsEnumerable.ToList(); - - for (var i = perms.Count - 1; i >= 0; i--) - { - var perm = perms[i]; - - var result = perm.CheckPermission(user, message, commandName, moduleName); - - if (result is null) - continue; - permIndex = i; - return result.Value; - } - - permIndex = -1; //defaut behaviour - return true; - } - - //null = not applicable - //true = applicable, allowed - //false = applicable, not allowed - public static bool? CheckPermission( - this Permissionv2 perm, - IUser user, - IMessageChannel channel, - string commandName, - string moduleName) - { - if (!((perm.SecondaryTarget == SecondaryPermissionType.Command - && string.Equals(perm.SecondaryTargetName, commandName, StringComparison.InvariantCultureIgnoreCase)) - || (perm.SecondaryTarget == SecondaryPermissionType.Module - && string.Equals(perm.SecondaryTargetName, moduleName, StringComparison.InvariantCultureIgnoreCase)) - || perm.SecondaryTarget == SecondaryPermissionType.AllModules)) - return null; - - var guildUser = user as IGuildUser; - - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - if (perm.PrimaryTargetId == user.Id) - return perm.State; - break; - case PrimaryPermissionType.Channel: - if (perm.PrimaryTargetId == channel.Id) - return perm.State; - break; - case PrimaryPermissionType.Role: - if (guildUser is null) - break; - if (guildUser.RoleIds.Contains(perm.PrimaryTargetId)) - return perm.State; - break; - case PrimaryPermissionType.Server: - if (guildUser is null) - break; - return perm.State; - } - - return null; - } - - public static string GetCommand(this Permissionv2 perm, string prefix, SocketGuild guild = null) - { - var com = string.Empty; - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - com += "u"; - break; - case PrimaryPermissionType.Channel: - com += "c"; - break; - case PrimaryPermissionType.Role: - com += "r"; - break; - case PrimaryPermissionType.Server: - com += "s"; - break; - } - - switch (perm.SecondaryTarget) - { - case SecondaryPermissionType.Module: - com += "m"; - break; - case SecondaryPermissionType.Command: - com += "c"; - break; - case SecondaryPermissionType.AllModules: - com = "a" + com + "m"; - break; - } - - var secName = perm.SecondaryTarget == SecondaryPermissionType.Command && !perm.IsCustomCommand - ? prefix + perm.SecondaryTargetName - : perm.SecondaryTargetName; - com += " " + (perm.SecondaryTargetName != "*" ? secName + " " : "") + (perm.State ? "enable" : "disable") + " "; - - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - com += guild?.GetUser(perm.PrimaryTargetId)?.ToString() ?? $"<@{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Channel: - com += $"<#{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Role: - com += guild?.GetRole(perm.PrimaryTargetId)?.ToString() ?? $"<@&{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Server: - break; - } - - return prefix + com; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/Permissions.cs b/src/Ellie.Bot.Modules.Permissions/Permissions.cs deleted file mode 100644 index 2a0dc4d..0000000 --- a/src/Ellie.Bot.Modules.Permissions/Permissions.cs +++ /dev/null @@ -1,516 +0,0 @@ -#nullable disable -using Ellie.Common.TypeReaders; -using Ellie.Common.TypeReaders.Models; -using Ellie.Db; -using Ellie.Modules.Permissions.Common; -using Ellie.Modules.Permissions.Services; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions : EllieModule -{ - public enum Reset { Reset } - - private readonly DbService _db; - - public Permissions(DbService db) - => _db = db; - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Verbose(PermissionAction action = null) - { - await using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsFor(ctx.Guild.Id); - if (action is null) - action = new(!config.VerbosePermissions); // New behaviour, can toggle. - config.VerbosePermissions = action.Value; - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.verbose_true); - else - await ReplyConfirmLocalizedAsync(strs.verbose_false); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public async Task PermRole([Leftover] IRole role = null) - { - if (role is not null && role == role.Guild.EveryoneRole) - return; - - if (role is null) - { - var cache = _service.GetCacheFor(ctx.Guild.Id); - if (!ulong.TryParse(cache.PermRole, out var roleId) - || (role = ((SocketGuild)ctx.Guild).GetRole(roleId)) is null) - await ReplyConfirmLocalizedAsync(strs.permrole_not_set); - else - await ReplyConfirmLocalizedAsync(strs.permrole(Format.Bold(role.ToString()))); - return; - } - - await using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsFor(ctx.Guild.Id); - config.PermissionRole = role.Id.ToString(); - uow.SaveChanges(); - _service.UpdateCache(config); - } - - await ReplyConfirmLocalizedAsync(strs.permrole_changed(Format.Bold(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public async Task PermRole(Reset _) - { - await using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsFor(ctx.Guild.Id); - config.PermissionRole = null; - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - - await ReplyConfirmLocalizedAsync(strs.permrole_reset); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ListPerms(int page = 1) - { - if (page < 1) - return; - - IList perms; - - if (_service.Cache.TryGetValue(ctx.Guild.Id, out var permCache)) - perms = permCache.Permissions.Source.ToList(); - else - perms = Permissionv2.GetDefaultPermlist; - - var startPos = 20 * (page - 1); - var toSend = Format.Bold(GetText(strs.page(page))) - + "\n\n" - + string.Join("\n", - perms.Reverse() - .Skip(startPos) - .Take(20) - .Select(p => - { - var str = - $"`{p.Index + 1}.` {Format.Bold(p.GetCommand(prefix, (SocketGuild)ctx.Guild))}"; - if (p.Index == 0) - str += $" [{GetText(strs.uneditable)}]"; - return str; - })); - - await ctx.Channel.SendMessageAsync(toSend); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RemovePerm(int index) - { - index -= 1; - if (index < 0) - return; - try - { - Permissionv2 p; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsFor(ctx.Guild.Id); - var permsCol = new PermissionsCollection(config.Permissions); - p = permsCol[index]; - permsCol.RemoveAt(index); - uow.Remove(p); - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - - await ReplyConfirmLocalizedAsync(strs.removed(index + 1, - Format.Code(p.GetCommand(prefix, (SocketGuild)ctx.Guild)))); - } - catch (IndexOutOfRangeException) - { - await ReplyErrorLocalizedAsync(strs.perm_out_of_range); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task MovePerm(int from, int to) - { - from -= 1; - to -= 1; - if (!(from == to || from < 0 || to < 0)) - { - try - { - Permissionv2 fromPerm; - await using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsFor(ctx.Guild.Id); - var permsCol = new PermissionsCollection(config.Permissions); - - var fromFound = @from < permsCol.Count; - var toFound = to < permsCol.Count; - - if (!fromFound) - { - await ReplyErrorLocalizedAsync(strs.perm_not_found(++@from)); - return; - } - - if (!toFound) - { - await ReplyErrorLocalizedAsync(strs.perm_not_found(++to)); - return; - } - - fromPerm = permsCol[@from]; - - permsCol.RemoveAt(@from); - permsCol.Insert(to, fromPerm); - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - - await ReplyConfirmLocalizedAsync(strs.moved_permission( - Format.Code(fromPerm.GetCommand(prefix, (SocketGuild)ctx.Guild)), - ++@from, - ++to)); - - return; - } - catch (Exception e) when (e is ArgumentOutOfRangeException or IndexOutOfRangeException) - { - } - } - - await ReplyConfirmLocalizedAsync(strs.perm_out_of_range); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SrvrCmd(CommandOrExprInfo command, PermissionAction action) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom - }); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(command.Name), GetText(strs.of_command))); - else - await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(command.Name), GetText(strs.of_command))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task SrvrMdl(ModuleOrCrInfo module, PermissionAction action) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value - }); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(module.Name), GetText(strs.of_module))); - else - await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(module.Name), GetText(strs.of_module))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task UsrCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IGuildUser user) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom - }); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(user.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(user.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task UsrMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IGuildUser user) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value - }); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(user.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(user.ToString()))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RoleCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IRole role) - { - if (role == role.Guild.EveryoneRole) - return; - - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom - }); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(role.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(role.Name))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task RoleMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IRole role) - { - if (role == role.Guild.EveryoneRole) - return; - - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value - }); - - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(role.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(role.Name))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ChnlCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] ITextChannel chnl) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom - }); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(chnl.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(chnl.Name))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task ChnlMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] ITextChannel chnl) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value - }); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(chnl.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(chnl.Name))); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AllChnlMdls(PermissionAction action, [Leftover] ITextChannel chnl) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value - }); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.acm_enable(Format.Code(chnl.Name))); - else - await ReplyConfirmLocalizedAsync(strs.acm_disable(Format.Code(chnl.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AllRoleMdls(PermissionAction action, [Leftover] IRole role) - { - if (role == role.Guild.EveryoneRole) - return; - - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value - }); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.arm_enable(Format.Code(role.Name))); - else - await ReplyConfirmLocalizedAsync(strs.arm_disable(Format.Code(role.Name))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AllUsrMdls(PermissionAction action, [Leftover] IUser user) - { - await _service.AddPermissions(ctx.Guild.Id, - new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value - }); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.aum_enable(Format.Code(user.ToString()))); - else - await ReplyConfirmLocalizedAsync(strs.aum_disable(Format.Code(user.ToString()))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AllSrvrMdls(PermissionAction action) - { - var newPerm = new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value - }; - - var allowUser = new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = ctx.User.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = true - }; - - await _service.AddPermissions(ctx.Guild.Id, newPerm, allowUser); - - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.asm_enable); - else - await ReplyConfirmLocalizedAsync(strs.asm_disable); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/PermissionsCollection.cs b/src/Ellie.Bot.Modules.Permissions/PermissionsCollection.cs deleted file mode 100644 index b902fd5..0000000 --- a/src/Ellie.Bot.Modules.Permissions/PermissionsCollection.cs +++ /dev/null @@ -1,74 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Permissions.Common; - -public class PermissionsCollection : IndexedCollection - where T : class, IIndexed -{ - public override T this[int index] - { - get => Source[index]; - set - { - lock (_localLocker) - { - if (index == 0) // can't set first element. It's always allow all - throw new IndexOutOfRangeException(nameof(index)); - base[index] = value; - } - } - } - - private readonly object _localLocker = new(); - - public PermissionsCollection(IEnumerable source) - : base(source) - { - } - - public static implicit operator List(PermissionsCollection x) - => x.Source; - - public override void Clear() - { - lock (_localLocker) - { - var first = Source[0]; - base.Clear(); - Source[0] = first; - } - } - - public override bool Remove(T item) - { - bool removed; - lock (_localLocker) - { - if (Source.IndexOf(item) == 0) - throw new ArgumentException("You can't remove first permsission (allow all)"); - removed = base.Remove(item); - } - - return removed; - } - - public override void Insert(int index, T item) - { - lock (_localLocker) - { - if (index == 0) // can't insert on first place. Last item is always allow all. - throw new IndexOutOfRangeException(nameof(index)); - base.Insert(index, item); - } - } - - public override void RemoveAt(int index) - { - lock (_localLocker) - { - if (index == 0) // you can't remove first permission (allow all) - throw new IndexOutOfRangeException(nameof(index)); - - base.RemoveAt(index); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/PermissionsService.cs b/src/Ellie.Bot.Modules.Permissions/PermissionsService.cs deleted file mode 100644 index d30c530..0000000 --- a/src/Ellie.Bot.Modules.Permissions/PermissionsService.cs +++ /dev/null @@ -1,184 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Modules.Permissions.Common; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Permissions.Services; - -public class PermissionService : IExecPreCommand, IEService -{ - public int Priority { get; } = 0; - - //guildid, root permission - public ConcurrentDictionary Cache { get; } = new(); - - private readonly DbService _db; - private readonly CommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly IEmbedBuilderService _eb; - - public PermissionService( - DiscordSocketClient client, - DbService db, - CommandHandler cmd, - IBotStrings strings, - IEmbedBuilderService eb) - { - _db = db; - _cmd = cmd; - _strings = strings; - _eb = eb; - - using var uow = _db.GetDbContext(); - foreach (var x in uow.Set().PermissionsForAll(client.Guilds.ToArray().Select(x => x.Id).ToList())) - { - Cache.TryAdd(x.GuildId, - new() - { - Verbose = x.VerbosePermissions, - PermRole = x.PermissionRole, - Permissions = new(x.Permissions) - }); - } - } - - public PermissionCache GetCacheFor(ulong guildId) - { - if (!Cache.TryGetValue(guildId, out var pc)) - { - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.Permissions)); - UpdateCache(config); - } - - Cache.TryGetValue(guildId, out pc); - if (pc is null) - throw new("Cache is null."); - } - - return pc; - } - - public async Task AddPermissions(ulong guildId, params Permissionv2[] perms) - { - await using var uow = _db.GetDbContext(); - var config = uow.GcWithPermissionsFor(guildId); - //var orderedPerms = new PermissionsCollection(config.Permissions); - var max = config.Permissions.Max(x => x.Index); //have to set its index to be the highest - foreach (var perm in perms) - { - perm.Index = ++max; - config.Permissions.Add(perm); - } - - await uow.SaveChangesAsync(); - UpdateCache(config); - } - - public void UpdateCache(GuildConfig config) - => Cache.AddOrUpdate(config.GuildId, - new PermissionCache - { - Permissions = new(config.Permissions), - PermRole = config.PermissionRole, - Verbose = config.VerbosePermissions - }, - (_, old) => - { - old.Permissions = new(config.Permissions); - old.PermRole = config.PermissionRole; - old.Verbose = config.VerbosePermissions; - return old; - }); - - public async Task ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command) - { - var guild = ctx.Guild; - var msg = ctx.Message; - var user = ctx.User; - var channel = ctx.Channel; - var commandName = command.Name.ToLowerInvariant(); - - if (guild is null) - return false; - - var resetCommand = commandName == "resetperms"; - - var pc = GetCacheFor(guild.Id); - if (!resetCommand - && !pc.Permissions.CheckPermissions(msg.Author, msg.Channel, commandName, moduleName, out var index)) - { - if (pc.Verbose) - { - try - { - await channel.SendErrorAsync(_eb, - _strings.GetText(strs.perm_prevent(index + 1, - Format.Bold(pc.Permissions[index] - .GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))), - guild.Id)); - } - catch - { - } - } - - return true; - } - - - if (moduleName == nameof(Permissions)) - { - if (user is not IGuildUser guildUser) - return true; - - if (guildUser.GuildPermissions.Administrator) - return false; - - var permRole = pc.PermRole; - if (!ulong.TryParse(permRole, out var rid)) - rid = 0; - string returnMsg; - IRole role; - if (string.IsNullOrWhiteSpace(permRole) || (role = guild.GetRole(rid)) is null) - { - returnMsg = "You need Admin permissions in order to use permission commands."; - if (pc.Verbose) - { - try { await channel.SendErrorAsync(_eb, returnMsg); } - catch { } - } - - return true; - } - - if (!guildUser.RoleIds.Contains(rid)) - { - returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands."; - if (pc.Verbose) - { - try { await channel.SendErrorAsync(_eb, returnMsg); } - catch { } - } - - return true; - } - - return false; - } - - return false; - } - - public async Task Reset(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var config = uow.GcWithPermissionsFor(guildId); - config.Permissions = Permissionv2.GetDefaultPermlist; - await uow.SaveChangesAsync(); - UpdateCache(config); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Permissions/ResetPermissionsCommands.cs b/src/Ellie.Bot.Modules.Permissions/ResetPermissionsCommands.cs deleted file mode 100644 index 9091db0..0000000 --- a/src/Ellie.Bot.Modules.Permissions/ResetPermissionsCommands.cs +++ /dev/null @@ -1,37 +0,0 @@ -#nullable disable -using Ellie.Modules.Permissions.Services; - -namespace Ellie.Modules.Permissions; - -public partial class Permissions -{ - [Group] - public partial class ResetPermissionsCommands : EllieModule - { - private readonly GlobalPermissionService _gps; - private readonly PermissionService _perms; - - public ResetPermissionsCommands(GlobalPermissionService gps, PermissionService perms) - { - _gps = gps; - _perms = perms; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ResetPerms() - { - await _perms.Reset(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.perms_reset); - } - - [Cmd] - [OwnerOnly] - public async Task ResetGlobalPerms() - { - await _gps.Reset(); - await ReplyConfirmLocalizedAsync(strs.global_perms_reset); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Anime/AnimeResult.cs b/src/Ellie.Bot.Modules.Searches/Anime/AnimeResult.cs deleted file mode 100644 index 7dab4a6..0000000 --- a/src/Ellie.Bot.Modules.Searches/Anime/AnimeResult.cs +++ /dev/null @@ -1,41 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches.Common; - -public class AnimeResult -{ - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("airing_status")] - public string AiringStatusParsed { get; set; } - - [JsonPropertyName("title_english")] - public string TitleEnglish { get; set; } - - [JsonPropertyName("total_episodes")] - public int TotalEpisodes { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("image_url_lge")] - public string ImageUrlLarge { get; set; } - - [JsonPropertyName("genres")] - public string[] Genres { get; set; } - - [JsonPropertyName("average_score")] - public float AverageScore { get; set; } - - - public string AiringStatus - => AiringStatusParsed.ToTitleCase(); - - public string Link - => "http://anilist.co/anime/" + Id; - - public string Synopsis - => Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "..."; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchCommands.cs b/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchCommands.cs deleted file mode 100644 index 5da8005..0000000 --- a/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchCommands.cs +++ /dev/null @@ -1,204 +0,0 @@ -#nullable disable -using AngleSharp; -using AngleSharp.Html.Dom; -using Ellie.Modules.Searches.Services; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class AnimeSearchCommands : EllieModule - { - // [NadekoCommand, Aliases] - // public async Task Novel([Leftover] string query) - // { - // if (string.IsNullOrWhiteSpace(query)) - // return; - // - // var novelData = await _service.GetNovelData(query); - // - // if (novelData is null) - // { - // await ReplyErrorLocalizedAsync(strs.failed_finding_novel); - // return; - // } - // - // var embed = _eb.Create() - // .WithOkColor() - // .WithDescription(novelData.Description.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) - // .WithTitle(novelData.Title) - // .WithUrl(novelData.Link) - // .WithImageUrl(novelData.ImageUrl) - // .AddField(GetText(strs.authors), string.Join("\n", novelData.Authors), true) - // .AddField(GetText(strs.status), novelData.Status, true) - // .AddField(GetText(strs.genres), string.Join(" ", novelData.Genres.Any() ? novelData.Genres : new[] { "none" }), true) - // .WithFooter($"{GetText(strs.score)} {novelData.Score}"); - // - // await ctx.Channel.EmbedAsync(embed); - // } - - [Cmd] - [Priority(0)] - public async Task Mal([Leftover] string name) - { - if (string.IsNullOrWhiteSpace(name)) - return; - - var fullQueryLink = "https://myanimelist.net/profile/" + name; - - var config = Configuration.Default.WithDefaultLoader(); - using var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink); - var imageElem = - document.QuerySelector( - "body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img"); - var imageUrl = ((IHtmlImageElement)imageElem)?.Source - ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png"; - - var stats = document - .QuerySelectorAll( - "body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span") - .Select(x => x.InnerHtml) - .ToList(); - - var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc"); - - var favAnime = GetText(strs.anime_no_fav); - if (favorites.Length > 0 && favorites[0].QuerySelector("p") is null) - { - favAnime = string.Join("\n", - favorites[0] - .QuerySelectorAll("ul > li > div.di-tc.va-t > a") - .Shuffle() - .Take(3) - .Select(x => - { - var elem = (IHtmlAnchorElement)x; - return $"[{elem.InnerHtml}]({elem.Href})"; - })); - } - - var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix") - .Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml)) - .ToList(); - - var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div") - .Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray()) - .ToArray(); - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.mal_profile(name))) - .AddField("💚 " + GetText(strs.watching), stats[0], true) - .AddField("💙 " + GetText(strs.completed), stats[1], true); - if (info.Count < 3) - embed.AddField("💛 " + GetText(strs.on_hold), stats[2], true); - embed.AddField("💔 " + GetText(strs.dropped), stats[3], true) - .AddField("⚪ " + GetText(strs.plan_to_watch), stats[4], true) - .AddField("🕐 " + daysAndMean[0][0], daysAndMean[0][1], true) - .AddField("📊 " + daysAndMean[1][0], daysAndMean[1][1], true) - .AddField(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1, info[0].Item2.TrimTo(20), true) - .AddField(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1, info[1].Item2.TrimTo(20), true); - if (info.Count > 2) - embed.AddField(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1, info[2].Item2.TrimTo(20), true); - - embed.WithDescription($@" -** https://myanimelist.net/animelist/{name} ** - -**{GetText(strs.top_3_fav_anime)}** -{favAnime}") - .WithUrl(fullQueryLink) - .WithImageUrl(imageUrl); - - await ctx.Channel.EmbedAsync(embed); - } - - private static string MalInfoToEmoji(string info) - { - info = info.Trim().ToLowerInvariant(); - switch (info) - { - case "gender": - return "🚁"; - case "location": - return "🗺"; - case "last online": - return "👥"; - case "birthday": - return "📆"; - default: - return "❔"; - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Mal(IGuildUser usr) - => Mal(usr.Username); - - [Cmd] - public async Task Anime([Leftover] string query) - { - if (string.IsNullOrWhiteSpace(query)) - return; - - var animeData = await _service.GetAnimeData(query); - - if (animeData is null) - { - await ReplyErrorLocalizedAsync(strs.failed_finding_anime); - return; - } - - var embed = _eb.Create() - .WithOkColor() - .WithDescription(animeData.Synopsis.Replace("
", - Environment.NewLine, - StringComparison.InvariantCulture)) - .WithTitle(animeData.TitleEnglish) - .WithUrl(animeData.Link) - .WithImageUrl(animeData.ImageUrlLarge) - .AddField(GetText(strs.episodes), animeData.TotalEpisodes.ToString(), true) - .AddField(GetText(strs.status), animeData.AiringStatus, true) - .AddField(GetText(strs.genres), - string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" }), - true) - .WithFooter($"{GetText(strs.score)} {animeData.AverageScore} / 100"); - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Manga([Leftover] string query) - { - if (string.IsNullOrWhiteSpace(query)) - return; - - var mangaData = await _service.GetMangaData(query); - - if (mangaData is null) - { - await ReplyErrorLocalizedAsync(strs.failed_finding_manga); - return; - } - - var embed = _eb.Create() - .WithOkColor() - .WithDescription(mangaData.Synopsis.Replace("
", - Environment.NewLine, - StringComparison.InvariantCulture)) - .WithTitle(mangaData.TitleEnglish) - .WithUrl(mangaData.Link) - .WithImageUrl(mangaData.ImageUrlLge) - .AddField(GetText(strs.chapters), mangaData.TotalChapters.ToString(), true) - .AddField(GetText(strs.status), mangaData.PublishingStatus, true) - .AddField(GetText(strs.genres), - string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" }), - true) - .WithFooter($"{GetText(strs.score)} {mangaData.AverageScore} / 100"); - - await ctx.Channel.EmbedAsync(embed); - } - } -} diff --git a/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchService.cs b/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchService.cs deleted file mode 100644 index b4f1580..0000000 --- a/src/Ellie.Bot.Modules.Searches/Anime/AnimeSearchService.cs +++ /dev/null @@ -1,79 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Common; -using System.Net.Http.Json; - -namespace Ellie.Modules.Searches.Services; - -public class AnimeSearchService : IEService -{ - private readonly IBotCache _cache; - private readonly IHttpClientFactory _httpFactory; - - public AnimeSearchService(IBotCache cache, IHttpClientFactory httpFactory) - { - _cache = cache; - _httpFactory = httpFactory; - } - - public async Task GetAnimeData(string query) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); - - TypedKey GetKey(string link) - => new TypedKey($"anime2:{link}"); - - try - { - var suffix = Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture)); - var link = $"https://aniapi.nadeko.bot/anime/{suffix}"; - link = link.ToLowerInvariant(); - var result = await _cache.GetAsync(GetKey(link)); - if (!result.TryPickT0(out var data, out _)) - { - using var http = _httpFactory.CreateClient(); - data = await http.GetFromJsonAsync(link); - - await _cache.AddAsync(GetKey(link), data, expiry: TimeSpan.FromHours(12)); - } - - return data; - } - catch - { - return null; - } - } - - public async Task GetMangaData(string query) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); - - TypedKey GetKey(string link) - => new TypedKey($"manga2:{link}"); - - try - { - var link = "https://aniapi.nadeko.bot/manga/" - + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture)); - link = link.ToLowerInvariant(); - - var result = await _cache.GetAsync(GetKey(link)); - if (!result.TryPickT0(out var data, out _)) - { - using var http = _httpFactory.CreateClient(); - data = await http.GetFromJsonAsync(link); - - await _cache.AddAsync(GetKey(link), data, expiry: TimeSpan.FromHours(3)); - } - - - return data; - } - catch - { - return null; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Anime/MangaResult.cs b/src/Ellie.Bot.Modules.Searches/Anime/MangaResult.cs deleted file mode 100644 index 7fce366..0000000 --- a/src/Ellie.Bot.Modules.Searches/Anime/MangaResult.cs +++ /dev/null @@ -1,40 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches.Common; - -public class MangaResult -{ - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("publishing_status")] - public string PublishingStatus { get; set; } - - [JsonPropertyName("image_url_lge")] - public string ImageUrlLge { get; set; } - - [JsonPropertyName("title_english")] - public string TitleEnglish { get; set; } - - [JsonPropertyName("total_chapters")] - public int TotalChapters { get; set; } - - [JsonPropertyName("total_volumes")] - public int TotalVolumes { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("genres")] - public string[] Genres { get; set; } - - [JsonPropertyName("average_score")] - public float AverageScore { get; set; } - - public string Link - => "http://anilist.co/manga/" + Id; - - public string Synopsis - => Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "..."; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/CryptoCommands.cs b/src/Ellie.Bot.Modules.Searches/Crypto/CryptoCommands.cs deleted file mode 100644 index 54bf006..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/CryptoCommands.cs +++ /dev/null @@ -1,196 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Services; -using System.Globalization; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - public partial class FinanceCommands : EllieModule - { - private readonly IStockDataService _stocksService; - private readonly IStockChartDrawingService _stockDrawingService; - - public FinanceCommands(IStockDataService stocksService, IStockChartDrawingService stockDrawingService) - { - _stocksService = stocksService; - _stockDrawingService = stockDrawingService; - } - - [Cmd] - public async Task Stock([Leftover]string query) - { - using var typing = ctx.Channel.EnterTypingState(); - - var stock = await _stocksService.GetStockDataAsync(query); - - if (stock is null) - { - var symbols = await _stocksService.SearchSymbolAsync(query); - - if (symbols.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - var symbol = symbols.First(); - var promptEmbed = _eb.Create() - .WithDescription(symbol.Description) - .WithTitle(GetText(strs.did_you_mean(symbol.Symbol))); - - if (!await PromptUserConfirmAsync(promptEmbed)) - return; - - query = symbol.Symbol; - stock = await _stocksService.GetStockDataAsync(query); - - if (stock is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - } - - var candles = await _stocksService.GetCandleDataAsync(query); - var stockImageTask = _stockDrawingService.GenerateCombinedChartAsync(candles); - - var localCulture = (CultureInfo)Culture.Clone(); - localCulture.NumberFormat.CurrencySymbol = "$"; - - var sign = stock.Price >= stock.Close - ? "\\🔼" - : "\\🔻"; - - var change = (stock.Price - stock.Close).ToString("N2", Culture); - var changePercent = (1 - (stock.Close / stock.Price)).ToString("P1", Culture); - - var sign50 = stock.Change50d >= 0 - ? "\\🔼" - : "\\🔻"; - - var change50 = (stock.Change50d).ToString("P1", Culture); - - var sign200 = stock.Change200d >= 0 - ? "\\🔼" - : "\\🔻"; - - var change200 = (stock.Change200d).ToString("P1", Culture); - - var price = stock.Price.ToString("C2", localCulture); - - var eb = _eb.Create() - .WithOkColor() - .WithAuthor(stock.Symbol) - .WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}") - .WithTitle(stock.Name) - .AddField(GetText(strs.price), $"{sign} **{price}**", true) - .AddField(GetText(strs.market_cap), stock.MarketCap.ToString("C0", localCulture), true) - .AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true) - .AddField("Change", $"{change} ({changePercent})", true) - .AddField("Change 50d", $"{sign50}{change50}", true) - .AddField("Change 200d", $"{sign200}{change200}", true) - .WithFooter(stock.Exchange); - - var message = await ctx.Channel.EmbedAsync(eb); - await using var imageData = await stockImageTask; - if (imageData is null) - return; - - var fileName = $"{query}-sparkline.{imageData.Extension}"; - using var attachment = new FileAttachment( - imageData.FileData, - fileName - ); - await message.ModifyAsync(mp => - { - mp.Attachments = - new(new[] - { - attachment - }); - - mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build(); - }); - } - - - [Cmd] - public async Task Crypto(string name) - { - name = name?.ToUpperInvariant(); - - if (string.IsNullOrWhiteSpace(name)) - return; - - var (crypto, nearest) = await _service.GetCryptoData(name); - - if (nearest is not null) - { - var embed = _eb.Create() - .WithTitle(GetText(strs.crypto_not_found)) - .WithDescription( - GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})")))); - - if (await PromptUserConfirmAsync(embed)) - crypto = nearest; - } - - if (crypto is null) - { - await ReplyErrorLocalizedAsync(strs.crypto_not_found); - return; - } - - var usd = crypto.Quote["USD"]; - - var localCulture = (CultureInfo)Culture.Clone(); - localCulture.NumberFormat.CurrencySymbol = "$"; - - var sevenDay = (usd.PercentChange7d / 100).ToString("P2", localCulture); - var lastDay = (usd.PercentChange24h / 100).ToString("P2", localCulture); - var price = usd.Price < 0.01 - ? usd.Price.ToString(localCulture) - : usd.Price.ToString("C2", localCulture); - - var volume = usd.Volume24h.ToString("C0", localCulture); - var marketCap = usd.MarketCap.ToString("C0", localCulture); - var dominance = (usd.MarketCapDominance / 100).ToString("P2", localCulture); - - await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0); - var fileName = $"{crypto.Slug}_7d.png"; - - var toSend = _eb.Create() - .WithOkColor() - .WithAuthor($"#{crypto.CmcRank}") - .WithTitle($"{crypto.Name} ({crypto.Symbol})") - .WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/") - .WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png") - .AddField(GetText(strs.market_cap), marketCap, true) - .AddField(GetText(strs.price), price, true) - .AddField(GetText(strs.volume_24h), volume, true) - .AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true) - .AddField(GetText(strs.market_cap_dominance), dominance, true) - .WithImageUrl($"attachment://{fileName}"); - - if (crypto.CirculatingSupply is double cs) - { - var csStr = cs.ToString("N0", localCulture); - - if (crypto.MaxSupply is double ms) - { - var perc = (cs / ms).ToString("P1", localCulture); - - toSend.AddField(GetText(strs.circulating_supply), $"{csStr} ({perc})", true); - } - else - { - toSend.AddField(GetText(strs.circulating_supply), csStr, true); - } - } - - - await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build()); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/CryptoService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/CryptoService.cs deleted file mode 100644 index 9532478..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/CryptoService.cs +++ /dev/null @@ -1,216 +0,0 @@ -#nullable enable -using Ellie.Modules.Searches.Common; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System.Globalization; -using System.Net.Http.Json; -using System.Xml; -using Color = SixLabors.ImageSharp.Color; -using StringExtensions = Ellie.Extensions.StringExtensions; - - -namespace Ellie.Modules.Searches.Services; - -public class CryptoService : IEService -{ - private readonly IBotCache _cache; - private readonly IHttpClientFactory _httpFactory; - private readonly IBotCredentials _creds; - - private readonly SemaphoreSlim _getCryptoLock = new(1, 1); - - public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds) - { - _cache = cache; - _httpFactory = httpFactory; - _creds = creds; - } - - private PointF[] GetSparklinePointsFromSvgText(string svgText) - { - var xml = new XmlDocument(); - xml.LoadXml(svgText); - - var gElement = xml["svg"]?["g"]; - if (gElement is null) - return Array.Empty(); - - Span points = new PointF[gElement.ChildNodes.Count]; - var cnt = 0; - - bool GetValuesFromAttributes( - XmlAttributeCollection attrs, - out float x1, - out float y1, - out float x2, - out float y2) - { - (x1, y1, x2, y2) = (0, 0, 0, 0); - return attrs["x1"]?.Value is string x1Str - && float.TryParse(x1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x1) - && attrs["y1"]?.Value is string y1Str - && float.TryParse(y1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y1) - && attrs["x2"]?.Value is string x2Str - && float.TryParse(x2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x2) - && attrs["y2"]?.Value is string y2Str - && float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2); - } - - foreach (XmlElement x in gElement.ChildNodes) - { - if (x.Name != "line") - continue; - - if (GetValuesFromAttributes(x.Attributes, out var x1, out var y1, out var x2, out var y2)) - { - points[cnt++] = new(x1, y1); - // this point will be set twice to the same value - // on all points except the last one - if (cnt + 1 < points.Length) - points[cnt + 1] = new(x2, y2); - } - } - - if (cnt == 0) - return Array.Empty(); - - return points.Slice(0, cnt).ToArray(); - } - - private SixLabors.ImageSharp.Image GenerateSparklineChart(PointF[] points, bool up) - { - const int width = 164; - const int height = 48; - - var img = new Image(width, height, Color.Transparent); - var color = up - ? Color.Green - : Color.FromRgb(220, 0, 0); - - img.Mutate(x => - { - x.DrawLines(color, 2, points); - }); - - return img; - } - - public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name) - { - if (string.IsNullOrWhiteSpace(name)) - return (null, null); - - name = name.ToUpperInvariant(); - var cryptos = await GetCryptoDataInternal(); - - if (cryptos is null or { Count: 0 }) - return (null, null); - - var crypto = cryptos.FirstOrDefault(x - => x.Slug.ToUpperInvariant() == name - || x.Name.ToUpperInvariant() == name - || x.Symbol.ToUpperInvariant() == name); - - if (crypto is not null) - return (crypto, null); - - - var nearest = cryptos - .Select(elem => (Elem: elem, - Distance: StringExtensions.LevenshteinDistance(elem.Name.ToUpperInvariant(), name))) - .OrderBy(x => x.Distance) - .FirstOrDefault(x => x.Distance <= 2); - - return (null, nearest.Elem); - } - - public async Task?> GetCryptoDataInternal() - { - await _getCryptoLock.WaitAsync(); - try - { - var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"), - async () => - { - try - { - using var http = _httpFactory.CreateClient(); - var data = await http.GetFromJsonAsync( - "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?" - + $"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}" - + "&start=1" - + "&limit=5000" - + "&convert=USD"); - - return data; - } - catch (Exception ex) - { - Log.Error(ex, "Error getting crypto data: {Message}", ex.Message); - return default; - } - }, - TimeSpan.FromHours(2)); - - if (data is null) - return default; - - return data.Data; - } - catch (Exception ex) - { - Log.Error(ex, "Error retreiving crypto data: {Message}", ex.Message); - return default; - } - finally - { - _getCryptoLock.Release(); - } - } - - private TypedKey GetSparklineKey(int id) - => new($"crypto:sparkline:{id}"); - - public async Task GetSparklineAsync(int id, bool up) - { - try - { - var bytes = await _cache.GetOrAddAsync(GetSparklineKey(id), - async () => - { - // if it fails, generate a new one - var points = await DownloadSparklinePointsAsync(id); - var sparkline = GenerateSparklineChart(points, up); - - using var stream = await sparkline.ToStreamAsync(); - return stream.ToArray(); - }, - TimeSpan.FromHours(1)); - - if (bytes is { Length: > 0 }) - { - return bytes.ToStream(); - } - - return default; - } - catch (Exception ex) - { - Log.Warning(ex, - "Exception occurred while downloading sparkline points: {ErrorMessage}", - ex.Message); - return default; - } - } - - private async Task DownloadSparklinePointsAsync(int id) - { - using var http = _httpFactory.CreateClient(); - var str = await http.GetStringAsync( - $"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg"); - var points = GetSparklinePointsFromSvgText(str); - return points; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs deleted file mode 100644 index 9bef0fd..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs +++ /dev/null @@ -1,103 +0,0 @@ -using CsvHelper; -using CsvHelper.Configuration; -using System.Globalization; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Ellie.Modules.Searches; - -public class DefaultStockDataService : IStockDataService, IEService -{ - private readonly IHttpClientFactory _httpClientFactory; - - public DefaultStockDataService(IHttpClientFactory httpClientFactory) - => _httpClientFactory = httpClientFactory; - - public async Task GetStockDataAsync(string query) - { - try - { - if (!query.IsAlphaNumeric()) - return default; - - using var http = _httpClientFactory.CreateClient(); - var data = await http.GetFromJsonAsync( - $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={query}"); - - if (data is null) - return default; - - var symbol = data.QuoteResponse.Result.FirstOrDefault(); - - if (symbol is null) - return default; - - return new() - { - Name = symbol.LongName, - Symbol = symbol.Symbol, - Price = symbol.RegularMarketPrice, - Close = symbol.RegularMarketPreviousClose, - MarketCap = symbol.MarketCap, - Change50d = symbol.FiftyDayAverageChangePercent, - Change200d = symbol.TwoHundredDayAverageChangePercent, - DailyVolume = symbol.AverageDailyVolume10Day, - Exchange = symbol.FullExchangeName - }; - } - catch (Exception) - { - // Log.Warning(ex, "Error getting stock data: {ErrorMessage}", ex.Message); - return default; - } - } - - public async Task> SearchSymbolAsync(string query) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); - - query = Uri.EscapeDataString(query); - - using var http = _httpClientFactory.CreateClient(); - - var res = await http.GetStringAsync( - "https://finance.yahoo.com/_finance_doubledown/api/resource/searchassist" - + $";searchTerm={query}" - + "?device=console"); - - var data = JsonSerializer.Deserialize(res); - - if (data is null or { Items: null }) - return Array.Empty(); - - return data.Items - .Where(x => x.Type == "S") - .Select(x => new SymbolData(x.Symbol, x.Name)) - .ToList(); - } - - private static CsvConfiguration _csvConfig = new(CultureInfo.InvariantCulture) - { - PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title) - }; - - // todo replace .ToTimestamp() and remove google protobuf dependency - // todo this needs testing - public async Task> GetCandleDataAsync(string query) - { - using var http = _httpClientFactory.CreateClient(); - await using var resStream = await http.GetStreamAsync( - $"https://query1.finance.yahoo.com/v7/finance/download/{query}" - + $"?period1={DateTime.UtcNow.Subtract(30.Days()).ToTimestamp()}" - + $"&period2={DateTime.UtcNow.ToTimestamp()}" - + "&interval=1d"); - - using var textReader = new StreamReader(resStream); - using var csv = new CsvReader(textReader, _csvConfig); - var records = csv.GetRecords().ToArray(); - - return records - .Map(static x => new CandleData(x.Open, x.Close, x.High, x.Low, x.Volume)); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/CandleDrawingData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/CandleDrawingData.cs deleted file mode 100644 index 6857d1c..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/CandleDrawingData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using SixLabors.ImageSharp; - -namespace Ellie.Modules.Searches; - -/// -/// All data required to draw a candle -/// -/// Whether the candle is green -/// Rectangle for the body -/// High line point -/// Low line point -public record CandleDrawingData(bool IsGreen, RectangleF BodyRect, PointF High, PointF Low); \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/IStockChartDrawingService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/IStockChartDrawingService.cs deleted file mode 100644 index 9bf7092..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/IStockChartDrawingService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie.Modules.Searches; - -public interface IStockChartDrawingService -{ - Task GenerateSparklineAsync(IReadOnlyCollection series); - Task GenerateCombinedChartAsync(IReadOnlyCollection series); - Task GenerateCandleChartAsync(IReadOnlyCollection series); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/ImagesharpStockChartDrawingService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/ImagesharpStockChartDrawingService.cs deleted file mode 100644 index e6840a0..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Drawing/ImagesharpStockChartDrawingService.cs +++ /dev/null @@ -1,202 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System.Runtime.CompilerServices; -using Color = SixLabors.ImageSharp.Color; - -namespace Ellie.Modules.Searches; - -public class ImagesharpStockChartDrawingService : IStockChartDrawingService, IEService -{ - private const int WIDTH = 300; - private const int HEIGHT = 100; - private const decimal MAX_HEIGHT = HEIGHT * 0.8m; - - private static readonly Rgba32 _backgroundColor = Rgba32.ParseHex("17181E"); - private static readonly Rgba32 _lineGuideColor = Rgba32.ParseHex("212125"); - private static readonly Rgba32 _sparklineColor = Rgba32.ParseHex("2961FC"); - private static readonly Rgba32 _greenBrush = Rgba32.ParseHex("26A69A"); - private static readonly Rgba32 _redBrush = Rgba32.ParseHex("EF5350"); - - public static float GetNormalizedPoint(decimal max, decimal point, decimal range) - => (float)((MAX_HEIGHT * ((max - point) / range)) + HeightOffset()); - - private PointF[] GetSparklinePointsInternal(IReadOnlyCollection series) - { - var candleStep = WIDTH / (series.Count + 1); - var max = series.Max(static x => x.High); - var min = series.Min(static x => x.Low); - - var range = max - min; - - var points = new PointF[series.Count]; - - var i = 0; - foreach (var candle in series) - { - var x = candleStep * (i + 1); - - var y = GetNormalizedPoint(max, candle.Close, range); - points[i++] = new(x, y); - } - - return points; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static decimal HeightOffset() - => (HEIGHT - MAX_HEIGHT) / 2m; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Image CreateCanvasInternal() - => new Image(WIDTH, HEIGHT, _backgroundColor); - - private CandleDrawingData[] GetChartDrawingDataInternal(IReadOnlyCollection series) - { - var candleMargin = 2; - var candleStep = (WIDTH - (candleMargin * series.Count)) / (series.Count + 1); - var max = series.Max(static x => x.High); - var min = series.Min(static x => x.Low); - - var range = max = min; - - var drawData = new CandleDrawingData[series.Count]; - - var candleWidth = candleStep; - - var i = 0; - foreach (var candle in series) - { - var offsetX = (i - 1) * candleMargin; - var x = (candleStep * (i = 1)) + offsetX; - var yOpen = GetNormalizedPoint(max, candle.Open, range); - var yClose = GetNormalizedPoint(max, candle.Close, range); - var y = candle.Open > candle.Close - ? yOpen - : yClose; - - var sizeH = Math.Abs(yOpen - yClose); - - var high = GetNormalizedPoint(max, candle.High, range); - var low = GetNormalizedPoint(max, candle.Low, range); - drawData[i] = new(candle.Open < candle.Close, - new(x, y, candleWidth, sizeH), - new(x + (candleStep / 2), high), - new(x + (candleStep / 2), low)); - ++i; - } - - return drawData; - } - - private void DrawChartData(Image image, CandleDrawingData[] drawData) - => image.Mutate(ctx => - { - foreach (var data in drawData) - DrawLineExtensions.DrawLines(ctx, - data.IsGreen - ? _greenBrush - : _redBrush, - 1, - data.High, - data.Low); - - - foreach (var data in drawData) - FillRectangleExtensions.Fill(ctx, - data.IsGreen - ? _greenBrush - : _redBrush, - data.BodyRect); - }); - - private void DrawLineGuides(Image image, IReadOnlyCollection series) - { - var max = series.Max(x => x.High); - var min = series.Min(x => x.Low); - - var step = (max - min) / 5; - - var lines = new float[6]; - - for (var i = 0; i < 6; i++) - { - var y = GetNormalizedPoint(max, min + (step * i), max - min); - lines[i] = y; - } - - image.Mutate(ctx => - { - // draw guides - foreach (var y in lines) - ctx.DrawLines(_lineGuideColor, 1, new PointF(0, y), new PointF(WIDTH, y)); - - // // draw min and max price on the chart - // ctx.DrawText(min.ToString(CultureInfo.InvariantCulture), - // SystemFonts.CreateFont("Arial", 5), - // Color.White, - // new PointF(0, (float)HeightOffset() - 5) - // ); - // - // ctx.DrawText(max.ToString("N1", CultureInfo.InvariantCulture), - // SystemFonts.CreateFont("Arial", 5), - // Color.White, - // new PointF(0, HEIGHT - (float)HeightOffset()) - // ); - }); - } - - public Task GenerateSparklineAsync(IReadOnlyCollection series) - { - if (series.Count == 0) - return Task.FromResult(default); - - using var image = CreateCanvasInternal(); - - var points = GetSparklinePointsInternal(series); - - image.Mutate(ctx => - { - ctx.DrawLines(_sparklineColor, 2, points); - }); - - return Task.FromResult(new("png", image.ToStream())); - } - - public Task GenerateCombinedChartAsync(IReadOnlyCollection series) - { - if (series.Count == 0) - return Task.FromResult(default); - - using var image = CreateCanvasInternal(); - - DrawLineGuides(image, series); - - var chartData = GetChartDrawingDataInternal(series); - DrawChartData(image, chartData); - - var points = GetSparklinePointsInternal(series); - image.Mutate(ctx => - { - ctx.DrawLines(Color.ParseHex("00FFFFAA"), 1, points); - }); - - return Task.FromResult(new("png", image.ToStream())); - } - - public Task GenerateCandleChartAsync(IReadOnlyCollection series) - { - if (series.Count == 0) - return Task.FromResult(default); - - using var image = CreateCanvasInternal(); - - DrawLineGuides(image, series); - - var drawData = GetChartDrawingDataInternal(series); - DrawChartData(image, drawData); - - return Task.FromResult(new("png", image.ToStream())); - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/IStockDataService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/IStockDataService.cs deleted file mode 100644 index c9f089e..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/IStockDataService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ellie.Modules.Searches; - -public interface IStockDataService -{ - public Task GetStockDataAsync(string symbol); - Task> SearchSymbolAsync(string query); - Task> GetCandleDataAsync(string query); -} diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResponse.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResponse.cs deleted file mode 100644 index 20e247a..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class FinnHubSearchResponse -{ - [JsonPropertyName("count")] - public int Count { get; set; } - - [JsonPropertyName("result")] - public List Result { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResult.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResult.cs deleted file mode 100644 index 9b79b86..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/FinnHubSearchResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class FinnHubSearchResult -{ - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("displaySymbol")] - public string DisplaySymbol { get; set; } - - [JsonPropertyName("symbol")] - public string Symbol { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonApiClient.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonApiClient.cs deleted file mode 100644 index f418d49..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonApiClient.cs +++ /dev/null @@ -1,55 +0,0 @@ -// using System.Net.Http.Json; -// -// namespace Ellie.Modules.Searches; -// -// public sealed class PolygonApiClient : IDisposable -// { -// private const string BASE_URL = "https://api.polygon.io/v3"; -// -// private readonly HttpClient _httpClient; -// private readonly string _apiKey; -// -// public PolygonApiClient(HttpClient httpClient, string apiKey) -// { -// _httpClient = httpClient; -// _apiKey = apiKey; -// } -// -// public async Task> TickersAsync(string? ticker = null, string? query = null) -// { -// if (string.IsNullOrWhiteSpace(query)) -// query = null; -// -// if(query is not null) -// query = Uri.EscapeDataString(query); -// -// var requestString = $"{BASE_URL}/reference/tickers" -// + "?type=CS" -// + "&active=true" -// + "&order=asc" -// + "&limit=1000" -// + $"&apiKey={_apiKey}"; -// -// if (!string.IsNullOrWhiteSpace(ticker)) -// requestString += $"&ticker={ticker}"; -// -// if (!string.IsNullOrWhiteSpace(query)) -// requestString += $"&search={query}"; -// -// -// var response = await _httpClient.GetFromJsonAsync(requestString); -// -// if (response is null) -// return Array.Empty(); -// -// return response.Results; -// } -// -// // public async Task TickerDetailsV3Async(string ticker) -// // { -// // return new(); -// // } -// -// public void Dispose() -// => _httpClient.Dispose(); -// } \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonStockDataService.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonStockDataService.cs deleted file mode 100644 index 47478cf..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonStockDataService.cs +++ /dev/null @@ -1,26 +0,0 @@ -// namespace Ellie.Modules.Searches; -// -// public sealed class PolygonStockDataService : IStockDataService -// { -// private readonly IHttpClientFactory _httpClientFactory; -// private readonly IBotCredsProvider _credsProvider; -// -// public PolygonStockDataService(IHttpClientFactory httpClientFactory, IBotCredsProvider credsProvider) -// { -// _httpClientFactory = httpClientFactory; -// _credsProvider = credsProvider; -// } -// -// public async Task> GetStockDataAsync(string? query = null) -// { -// using var httpClient = _httpClientFactory.CreateClient(); -// using var client = new PolygonApiClient(httpClient, string.Empty); -// var data = await client.TickersAsync(query: query); -// -// return data.Map(static x => new StockData() -// { -// Name = x.Name, -// Ticker = x.Ticker, -// }); -// } -// } \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerData.cs deleted file mode 100644 index d09f9ed..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerData.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class PolygonTickerData -{ - [JsonPropertyName("ticker")] - public string Ticker { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("market")] - public string Market { get; set; } - - [JsonPropertyName("locale")] - public string Locale { get; set; } - - [JsonPropertyName("primary_exchange")] - public string PrimaryExchange { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("active")] - public bool Active { get; set; } - - [JsonPropertyName("currency_name")] - public string CurrencyName { get; set; } - - [JsonPropertyName("cik")] - public string Cik { get; set; } - - [JsonPropertyName("composite_figi")] - public string CompositeFigi { get; set; } - - [JsonPropertyName("share_class_figi")] - public string ShareClassFigi { get; set; } - - [JsonPropertyName("last_updated_utc")] - public DateTime LastUpdatedUtc { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerResponse.cs b/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerResponse.cs deleted file mode 100644 index 13575e1..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/Polygon/PolygonTickerResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class PolygonTickerResponse -{ - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("results")] - public List Results { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/CandleData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/CandleData.cs deleted file mode 100644 index b277e0e..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/CandleData.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ellie.Modules.Searches; - -public record CandleData -( - decimal Open, - decimal Close, - decimal High, - decimal Low, - long Volume); diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/ImageData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/ImageData.cs deleted file mode 100644 index 3e3c25b..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/ImageData.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Modules.Searches; - -public record ImageData(string Extension, Stream FileData) : IAsyncDisposable -{ - public ValueTask DisposeAsync() - => FileData.DisposeAsync(); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/QuoteResponse.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/QuoteResponse.cs deleted file mode 100644 index 8293b69..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/QuoteResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class QuoteResponse -{ - public class ResultModel - { - [JsonPropertyName("longName")] - public string LongName { get; set; } - - [JsonPropertyName("regularMarketPrice")] - public double RegularMarketPrice { get; set; } - - [JsonPropertyName("regularMarketPreviousClose")] - public double RegularMarketPreviousClose { get; set; } - - [JsonPropertyName("fullExchangeName")] - public string FullExchangeName { get; set; } - - [JsonPropertyName("averageDailyVolume10Day")] - public int AverageDailyVolume10Day { get; set; } - - [JsonPropertyName("fiftyDayAverageChangePercent")] - public double FiftyDayAverageChangePercent { get; set; } - - [JsonPropertyName("twoHundredDayAverageChangePercent")] - public double TwoHundredDayAverageChangePercent { get; set; } - - [JsonPropertyName("marketCap")] - public long MarketCap { get; set; } - - [JsonPropertyName("symbol")] - public string Symbol { get; set; } - } - - [JsonPropertyName("result")] - public List Result { get; set; } - - [JsonPropertyName("error")] - public object Error { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/StockData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/StockData.cs deleted file mode 100644 index f1ca9f1..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/StockData.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Searches; - -public class StockData -{ - public string Name { get; set; } - public string Symbol { get; set; } - public double Price { get; set; } - public long MarketCap { get; set; } - public double Close { get; set; } - public double Change50d { get; set; } - public double Change200d { get; set; } - public long DailyVolume { get; set; } - public string Exchange { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/SymbolData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/SymbolData.cs deleted file mode 100644 index 880b110..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/SymbolData.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Ellie.Modules.Searches; - -public record SymbolData(string Symbol, string Description); \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceCandleData.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceCandleData.cs deleted file mode 100644 index 95b428d..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceCandleData.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ellie.Modules.Searches; - -public class YahooFinanceCandleData -{ - public DateTime Date { get; set; } - public decimal Open { get; set; } - public decimal High { get; set; } - public decimal Low { get; set; } - public decimal Close { get; set; } - public decimal AdjClose { get; set; } - public long Volume { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponse.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponse.cs deleted file mode 100644 index 6b26b53..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class YahooFinanceSearchResponse -{ - [JsonPropertyName("suggestionTitleAccessor")] - public string SuggestionTitleAccessor { get; set; } - - [JsonPropertyName("suggestionMeta")] - public List SuggestionMeta { get; set; } - - [JsonPropertyName("hiConf")] - public bool HiConf { get; set; } - - [JsonPropertyName("items")] - public List Items { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponseItem.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponseItem.cs deleted file mode 100644 index 2ec9551..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooFinanceSearchResponseItem.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class YahooFinanceSearchResponseItem -{ - [JsonPropertyName("symbol")] - public string Symbol { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("exch")] - public string Exch { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("exchDisp")] - public string ExchDisp { get; set; } - - [JsonPropertyName("typeDisp")] - public string TypeDisp { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooQueryModel.cs b/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooQueryModel.cs deleted file mode 100644 index 9e429b5..0000000 --- a/src/Ellie.Bot.Modules.Searches/Crypto/_common/YahooQueryModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches; - -public class YahooQueryModel -{ - [JsonPropertyName("quoteResponse")] - public QuoteResponse QuoteResponse { get; set; } = null; -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Feeds/FeedCommands.cs b/src/Ellie.Bot.Modules.Searches/Feeds/FeedCommands.cs deleted file mode 100644 index 342266b..0000000 --- a/src/Ellie.Bot.Modules.Searches/Feeds/FeedCommands.cs +++ /dev/null @@ -1,115 +0,0 @@ -#nullable disable -using CodeHollow.FeedReader; -using Ellie.Modules.Searches.Services; -using System.Text.RegularExpressions; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class FeedCommands : EllieModule - { - private static readonly Regex _ytChannelRegex = - new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?[a-zA-Z0-9\-_]{1,})"); - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public Task YtUploadNotif(string url, ITextChannel channel = null, [Leftover] string message = null) - { - var m = _ytChannelRegex.Match(url); - if (!m.Success) - return ReplyErrorLocalizedAsync(strs.invalid_input); - - var channelId = m.Groups["channelid"].Value; - - return Feed($"https://www.youtube.com/feeds/videos.xml?channel_id={channelId}", channel, message); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task Feed(string url, ITextChannel channel = null, [Leftover] string message = null) - { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) - || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) - { - await ReplyErrorLocalizedAsync(strs.feed_invalid_url); - return; - } - - channel ??= (ITextChannel)ctx.Channel; - try - { - await FeedReader.ReadAsync(url); - } - catch (Exception ex) - { - Log.Information(ex, "Unable to get feeds from that url"); - await ReplyErrorLocalizedAsync(strs.feed_cant_parse); - return; - } - - if (ctx.User is not IGuildUser gu || !gu.GuildPermissions.Administrator) - message = message?.SanitizeMentions(true); - - var result = _service.AddFeed(ctx.Guild.Id, channel.Id, url, message); - if (result == FeedAddResult.Success) - { - await ReplyConfirmLocalizedAsync(strs.feed_added); - return; - } - - if (result == FeedAddResult.Duplicate) - { - await ReplyErrorLocalizedAsync(strs.feed_duplicate); - return; - } - - if (result == FeedAddResult.LimitReached) - { - await ReplyErrorLocalizedAsync(strs.feed_limit_reached); - return; - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task FeedRemove(int index) - { - if (_service.RemoveFeed(ctx.Guild.Id, --index)) - await ReplyConfirmLocalizedAsync(strs.feed_removed); - else - await ReplyErrorLocalizedAsync(strs.feed_out_of_range); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task FeedList() - { - var feeds = _service.GetFeeds(ctx.Guild.Id); - - if (!feeds.Any()) - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor().WithDescription(GetText(strs.feed_no_feed))); - return; - } - - await ctx.SendPaginatedConfirmAsync(0, - cur => - { - var embed = _eb.Create().WithOkColor(); - var i = 0; - var fs = string.Join("\n", - feeds.Skip(cur * 10).Take(10).Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}")); - - return embed.WithDescription(fs); - }, - feeds.Count, - 10); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Feeds/FeedsService.cs b/src/Ellie.Bot.Modules.Searches/Feeds/FeedsService.cs deleted file mode 100644 index 8917ea7..0000000 --- a/src/Ellie.Bot.Modules.Searches/Feeds/FeedsService.cs +++ /dev/null @@ -1,280 +0,0 @@ -#nullable disable -using CodeHollow.FeedReader; -using CodeHollow.FeedReader.Feeds; -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Searches.Services; - -public class FeedsService : IEService -{ - private readonly DbService _db; - private readonly ConcurrentDictionary> _subs; - private readonly DiscordSocketClient _client; - private readonly IEmbedBuilderService _eb; - - private readonly ConcurrentDictionary _lastPosts = new(); - private readonly Dictionary _errorCounters = new(); - - public FeedsService( - IBot bot, - DbService db, - DiscordSocketClient client, - IEmbedBuilderService eb) - { - _db = db; - - using (var uow = db.GetDbContext()) - { - var guildConfigIds = bot.AllGuildConfigs.Select(x => x.Id).ToList(); - _subs = uow.Set() - .AsQueryable() - .Where(x => guildConfigIds.Contains(x.Id)) - .Include(x => x.FeedSubs) - .ToList() - .SelectMany(x => x.FeedSubs) - .GroupBy(x => x.Url.ToLower()) - .ToDictionary(x => x.Key, x => x.ToList()) - .ToConcurrent(); - } - - _client = client; - _eb = eb; - - _ = 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); - } - - return newValue; - } - catch (Exception ex) - { - Log.Error(ex, "Error adding rss errors..."); - return 0; - } - } - - public async Task TrackFeeds() - { - while (true) - { - var allSendTasks = new List(_subs.Count); - foreach (var kvp in _subs) - { - if (kvp.Value.Count == 0) - continue; - - var rssUrl = kvp.Value.First().Url; - try - { - var feed = await FeedReader.ReadAsync(rssUrl); - - var items = feed - .Items.Select(item => (Item: item, - LastUpdate: item.PublishingDate?.ToUniversalTime() - ?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate?.ToUniversalTime())) - .Where(data => data.LastUpdate is not null) - .Select(data => (data.Item, LastUpdate: (DateTime)data.LastUpdate)) - .OrderByDescending(data => data.LastUpdate) - .Reverse() // start from the oldest - .ToList(); - - if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate)) - { - lastFeedUpdate = _lastPosts[kvp.Key] = - items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow; - } - - foreach (var (feedItem, itemUpdateDate) in items) - { - if (itemUpdateDate <= lastFeedUpdate) - continue; - - var embed = _eb.Create().WithFooter(rssUrl); - - _lastPosts[kvp.Key] = itemUpdateDate; - - var link = feedItem.SpecificItem.Link; - if (!string.IsNullOrWhiteSpace(link) && Uri.IsWellFormedUriString(link, UriKind.Absolute)) - embed.WithUrl(link); - - var title = string.IsNullOrWhiteSpace(feedItem.Title) ? "-" : feedItem.Title; - - var gotImage = false; - if (feedItem.SpecificItem is MediaRssFeedItem mrfi - && (mrfi.Enclosure?.MediaType?.StartsWith("image/") ?? false)) - { - var imgUrl = mrfi.Enclosure.Url; - if (!string.IsNullOrWhiteSpace(imgUrl) - && Uri.IsWellFormedUriString(imgUrl, UriKind.Absolute)) - { - embed.WithImageUrl(imgUrl); - gotImage = true; - } - } - - if (!gotImage && feedItem.SpecificItem is AtomFeedItem afi) - { - var previewElement = afi.Element.Elements() - .FirstOrDefault(x => x.Name.LocalName == "preview"); - - if (previewElement is null) - { - previewElement = afi.Element.Elements() - .FirstOrDefault(x => x.Name.LocalName == "thumbnail"); - } - - if (previewElement is not null) - { - var urlAttribute = previewElement.Attribute("url"); - if (urlAttribute is not null - && !string.IsNullOrWhiteSpace(urlAttribute.Value) - && Uri.IsWellFormedUriString(urlAttribute.Value, UriKind.Absolute)) - { - embed.WithImageUrl(urlAttribute.Value); - gotImage = true; - } - } - } - - embed.WithTitle(title.TrimTo(256)); - - var desc = feedItem.Description?.StripHtml(); - if (!string.IsNullOrWhiteSpace(feedItem.Description)) - embed.WithDescription(desc.TrimTo(2048)); - - //send the created embed to all subscribed channels - var feedSendTasks = kvp.Value - .Where(x => x.GuildConfig is not null) - .Select(x => _client.GetGuild(x.GuildConfig.GuildId) - ?.GetTextChannel(x.ChannelId) - ?.EmbedAsync(embed, x.Message)) - .Where(x => x is not null); - - allSendTasks.Add(feedSendTasks.WhenAll()); - - // as data retrieval was successful, reset error counter - ClearErrors(rssUrl); - } - } - catch (Exception ex) - { - var errorCount = await AddError(rssUrl, kvp.Value.Select(x => x.Id).ToList()); - - Log.Warning("An error occured while getting rss stream ({ErrorCount} / 100) {RssFeed}" - + "\n {Message}", - errorCount, - rssUrl, - $"[{ex.GetType().Name}]: {ex.Message}"); - } - } - - await Task.WhenAll(Task.WhenAll(allSendTasks), Task.Delay(30000)); - } - } - - public List GetFeeds(ulong guildId) - { - using var uow = _db.GetDbContext(); - return uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs)) - .FeedSubs.OrderBy(x => x.Id) - .ToList(); - } - - public FeedAddResult AddFeed(ulong guildId, ulong channelId, string rssFeed, string message) - { - ArgumentNullException.ThrowIfNull(rssFeed, nameof(rssFeed)); - - var fs = new FeedSub - { - ChannelId = channelId, - Url = rssFeed.Trim() - }; - - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs)); - - if (gc.FeedSubs.Any(x => x.Url.ToLower() == fs.Url.ToLower())) - return FeedAddResult.Duplicate; - if (gc.FeedSubs.Count >= 10) - return FeedAddResult.LimitReached; - - gc.FeedSubs.Add(fs); - uow.SaveChanges(); - //adding all, in case bot wasn't on this guild when it started - foreach (var feed in gc.FeedSubs) - { - _subs.AddOrUpdate(feed.Url.ToLower(), - new List - { - feed - }, - (_, old) => - { - old.Add(feed); - return old; - }); - } - - return FeedAddResult.Success; - } - - public bool RemoveFeed(ulong guildId, int index) - { - if (index < 0) - return false; - - using var uow = _db.GetDbContext(); - var items = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs)) - .FeedSubs.OrderBy(x => x.Id) - .ToList(); - - if (items.Count <= index) - return false; - var toRemove = items[index]; - _subs.AddOrUpdate(toRemove.Url.ToLower(), - new List(), - (_, old) => - { - old.Remove(toRemove); - return old; - }); - uow.Remove(toRemove); - uow.SaveChanges(); - - return true; - } -} - -public enum FeedAddResult -{ - Success, - LimitReached, - Invalid, - Duplicate, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/JokeCommands.cs b/src/Ellie.Bot.Modules.Searches/JokeCommands.cs deleted file mode 100644 index f463c1e..0000000 --- a/src/Ellie.Bot.Modules.Searches/JokeCommands.cs +++ /dev/null @@ -1,53 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Services; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public class JokeCommands : EllieModule - { - [Cmd] - public async Task Yomama() - => await SendConfirmAsync(await _service.GetYomamaJoke()); - - [Cmd] - public async Task Randjoke() - { - var (setup, punchline) = await _service.GetRandomJoke(); - await SendConfirmAsync(setup, punchline); - } - - [Cmd] - public async Task ChuckNorris() - => await SendConfirmAsync(await _service.GetChuckNorrisJoke()); - - [Cmd] - public async Task WowJoke() - { - if (!_service.WowJokes.Any()) - { - await ReplyErrorLocalizedAsync(strs.jokes_not_loaded); - return; - } - - var joke = _service.WowJokes[new EllieRandom().Next(0, _service.WowJokes.Count)]; - await SendConfirmAsync(joke.Question, joke.Answer); - } - - [Cmd] - public async Task MagicItem() - { - if (!_service.MagicItems.Any()) - { - await ReplyErrorLocalizedAsync(strs.magicitems_not_loaded); - return; - } - - var item = _service.MagicItems[new EllieRandom().Next(0, _service.MagicItems.Count)]; - - await SendConfirmAsync("✨" + item.Name, item.Description); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/MemegenCommands.cs b/src/Ellie.Bot.Modules.Searches/MemegenCommands.cs deleted file mode 100644 index eff7266..0000000 --- a/src/Ellie.Bot.Modules.Searches/MemegenCommands.cs +++ /dev/null @@ -1,96 +0,0 @@ -#nullable disable -using Newtonsoft.Json; -using System.Collections.Immutable; -using System.Text; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class MemegenCommands : EllieModule - { - private static readonly ImmutableDictionary _map = new Dictionary - { - { '?', "~q" }, - { '%', "~p" }, - { '#', "~h" }, - { '/', "~s" }, - { ' ', "-" }, - { '-', "--" }, - { '_', "__" }, - { '"', "''" } - }.ToImmutableDictionary(); - - private readonly IHttpClientFactory _httpFactory; - - public MemegenCommands(IHttpClientFactory factory) - => _httpFactory = factory; - - [Cmd] - public async Task Memelist(int page = 1) - { - if (--page < 0) - return; - - using var http = _httpFactory.CreateClient("memelist"); - using var res = await http.GetAsync("https://api.memegen.link/templates/"); - - var rawJson = await res.Content.ReadAsStringAsync(); - - var data = JsonConvert.DeserializeObject>(rawJson)!; - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var templates = string.Empty; - foreach (var template in data.Skip(curPage * 15).Take(15)) - templates += $"**{template.Name}:**\n key: `{template.Id}`\n"; - var embed = _eb.Create().WithOkColor().WithDescription(templates); - - return embed; - }, - data.Count, - 15); - } - - [Cmd] - public async Task Memegen(string meme, [Leftover] string memeText = null) - { - var memeUrl = $"http://api.memegen.link/{meme}"; - if (!string.IsNullOrWhiteSpace(memeText)) - { - var memeTextArray = memeText.Split(';'); - foreach (var text in memeTextArray) - { - var newText = Replace(text); - memeUrl += $"/{newText}"; - } - } - - memeUrl += ".png"; - await ctx.Channel.SendMessageAsync(memeUrl); - } - - private static string Replace(string input) - { - var sb = new StringBuilder(); - - foreach (var c in input) - { - if (_map.TryGetValue(c, out var tmp)) - sb.Append(tmp); - else - sb.Append(c); - } - - return sb.ToString(); - } - - private class MemegenTemplate - { - public string Name { get; set; } - public string Id { get; set; } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/OsuCommands.cs b/src/Ellie.Bot.Modules.Searches/OsuCommands.cs deleted file mode 100644 index 7e9477f..0000000 --- a/src/Ellie.Bot.Modules.Searches/OsuCommands.cs +++ /dev/null @@ -1,297 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Common; -using Newtonsoft.Json; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class OsuCommands : EllieModule - { - private readonly IBotCredentials _creds; - private readonly IHttpClientFactory _httpFactory; - - public OsuCommands(IBotCredentials creds, IHttpClientFactory factory) - { - _creds = creds; - _httpFactory = factory; - } - - [Cmd] - public async Task Osu(string user, [Leftover] string mode = null) - { - if (string.IsNullOrWhiteSpace(user)) - return; - - using var http = _httpFactory.CreateClient(); - var modeNumber = string.IsNullOrWhiteSpace(mode) ? 0 : ResolveGameMode(mode); - - try - { - if (string.IsNullOrWhiteSpace(_creds.OsuApiKey)) - { - await ReplyErrorLocalizedAsync(strs.osu_api_key); - return; - } - - var smode = ResolveGameMode(modeNumber); - var userReq = $"https://osu.ppy.sh/api/get_user?k={_creds.OsuApiKey}&u={user}&m={modeNumber}"; - var userResString = await http.GetStringAsync(userReq); - var objs = JsonConvert.DeserializeObject>(userResString); - - if (objs.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.osu_user_not_found); - return; - } - - var obj = objs[0]; - var userId = obj.UserId; - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle($"osu! {smode} profile for {user}") - .WithThumbnailUrl($"https://a.ppy.sh/{userId}") - .WithDescription($"https://osu.ppy.sh/u/{userId}") - .AddField("Official Rank", $"#{obj.PpRank}", true) - .AddField("Country Rank", - $"#{obj.PpCountryRank} :flag_{obj.Country.ToLower()}:", - true) - .AddField("Total PP", Math.Round(obj.PpRaw, 2), true) - .AddField("Accuracy", Math.Round(obj.Accuracy, 2) + "%", true) - .AddField("Playcount", obj.Playcount, true) - .AddField("Level", Math.Round(obj.Level), true)); - } - catch (ArgumentOutOfRangeException) - { - await ReplyErrorLocalizedAsync(strs.osu_user_not_found); - } - catch (Exception ex) - { - await ReplyErrorLocalizedAsync(strs.osu_failed); - Log.Warning(ex, "Osu command failed"); - } - } - - [Cmd] - public async Task Gatari(string user, [Leftover] string mode = null) - { - using var http = _httpFactory.CreateClient(); - var modeNumber = string.IsNullOrWhiteSpace(mode) ? 0 : ResolveGameMode(mode); - - var modeStr = ResolveGameMode(modeNumber); - var resString = await http.GetStringAsync($"https://api.gatari.pw/user/stats?u={user}&mode={modeNumber}"); - - var statsResponse = JsonConvert.DeserializeObject(resString); - if (statsResponse.Code != 200 || statsResponse.Stats.Id == 0) - { - await ReplyErrorLocalizedAsync(strs.osu_user_not_found); - return; - } - - var usrResString = await http.GetStringAsync($"https://api.gatari.pw/users/get?u={user}"); - - var userData = JsonConvert.DeserializeObject(usrResString).Users[0]; - var userStats = statsResponse.Stats; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle($"osu!Gatari {modeStr} profile for {user}") - .WithThumbnailUrl($"https://a.gatari.pw/{userStats.Id}") - .WithDescription($"https://osu.gatari.pw/u/{userStats.Id}") - .AddField("Official Rank", $"#{userStats.Rank}", true) - .AddField("Country Rank", - $"#{userStats.CountryRank} :flag_{userData.Country.ToLower()}:", - true) - .AddField("Total PP", userStats.Pp, true) - .AddField("Accuracy", $"{Math.Round(userStats.AvgAccuracy, 2)}%", true) - .AddField("Playcount", userStats.Playcount, true) - .AddField("Level", userStats.Level, true); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - public async Task Osu5(string user, [Leftover] string mode = null) - { - if (string.IsNullOrWhiteSpace(_creds.OsuApiKey)) - { - await SendErrorAsync("An osu! API key is required."); - return; - } - - if (string.IsNullOrWhiteSpace(user)) - { - await SendErrorAsync("Please provide a username."); - return; - } - - using var http = _httpFactory.CreateClient(); - var m = 0; - if (!string.IsNullOrWhiteSpace(mode)) - m = ResolveGameMode(mode); - - var reqString = "https://osu.ppy.sh/api/get_user_best" - + $"?k={_creds.OsuApiKey}" - + $"&u={Uri.EscapeDataString(user)}" - + "&type=string" - + "&limit=5" - + $"&m={m}"; - - var resString = await http.GetStringAsync(reqString); - var obj = JsonConvert.DeserializeObject>(resString); - - var mapTasks = obj.Select(async item => - { - var mapReqString = "https://osu.ppy.sh/api/get_beatmaps" - + $"?k={_creds.OsuApiKey}" - + $"&b={item.BeatmapId}"; - - var mapResString = await http.GetStringAsync(mapReqString); - var map = JsonConvert.DeserializeObject>(mapResString).FirstOrDefault(); - if (map is null) - return default; - var pp = Math.Round(item.Pp, 2); - var acc = CalculateAcc(item, m); - var mods = ResolveMods(item.EnabledMods); - - var title = $"{map.Artist}-{map.Title} ({map.Version})"; - var desc = $@"[/b/{item.BeatmapId}](https://osu.ppy.sh/b/{item.BeatmapId}) -{pp + "pp",-7} | {acc + "%",-7} -"; - if (mods != "+") - desc += Format.Bold(mods); - - return (title, desc); - }); - - var eb = _eb.Create().WithOkColor().WithTitle($"Top 5 plays for {user}"); - - var mapData = await mapTasks.WhenAll(); - foreach (var (title, desc) in mapData.Where(x => x != default)) - eb.AddField(title, desc); - - await ctx.Channel.EmbedAsync(eb); - } - - //https://osu.ppy.sh/wiki/Accuracy - private static double CalculateAcc(OsuUserBests play, int mode) - { - double hitPoints; - double totalHits; - if (mode == 0) - { - hitPoints = (play.Count50 * 50) + (play.Count100 * 100) + (play.Count300 * 300); - totalHits = play.Count50 + play.Count100 + play.Count300 + play.Countmiss; - totalHits *= 300; - } - else if (mode == 1) - { - hitPoints = (play.Countmiss * 0) + (play.Count100 * 0.5) + play.Count300; - totalHits = (play.Countmiss + play.Count100 + play.Count300) * 300; - hitPoints *= 300; - } - else if (mode == 2) - { - hitPoints = play.Count50 + play.Count100 + play.Count300; - totalHits = play.Countmiss + play.Count50 + play.Count100 + play.Count300 + play.Countkatu; - } - else - { - hitPoints = (play.Count50 * 50) - + (play.Count100 * 100) - + (play.Countkatu * 200) - + ((play.Count300 + play.Countgeki) * 300); - - totalHits = (play.Countmiss - + play.Count50 - + play.Count100 - + play.Countkatu - + play.Count300 - + play.Countgeki) - * 300; - } - - - return Math.Round(hitPoints / totalHits * 100, 2); - } - - private static int ResolveGameMode(string mode) - { - switch (mode.ToUpperInvariant()) - { - case "STD": - case "STANDARD": - return 0; - case "TAIKO": - return 1; - case "CTB": - case "CATCHTHEBEAT": - return 2; - case "MANIA": - case "OSU!MANIA": - return 3; - default: - return 0; - } - } - - private static string ResolveGameMode(int mode) - { - switch (mode) - { - case 0: - return "Standard"; - case 1: - return "Taiko"; - case 2: - return "Catch"; - case 3: - return "Mania"; - default: - return "Standard"; - } - } - - //https://github.com/ppy/osu-api/wiki#mods - private static string ResolveMods(int mods) - { - var modString = "+"; - - if (IsBitSet(mods, 0)) - modString += "NF"; - if (IsBitSet(mods, 1)) - modString += "EZ"; - if (IsBitSet(mods, 8)) - modString += "HT"; - - if (IsBitSet(mods, 3)) - modString += "HD"; - if (IsBitSet(mods, 4)) - modString += "HR"; - if (IsBitSet(mods, 6) && !IsBitSet(mods, 9)) - modString += "DT"; - if (IsBitSet(mods, 9)) - modString += "NC"; - if (IsBitSet(mods, 10)) - modString += "FL"; - - if (IsBitSet(mods, 5)) - modString += "SD"; - if (IsBitSet(mods, 14)) - modString += "PF"; - - if (IsBitSet(mods, 7)) - modString += "RX"; - if (IsBitSet(mods, 11)) - modString += "AT"; - if (IsBitSet(mods, 12)) - modString += "SO"; - return modString; - } - - private static bool IsBitSet(int mods, int pos) - => (mods & (1 << pos)) != 0; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/PathOfExileCommands.cs b/src/Ellie.Bot.Modules.Searches/PathOfExileCommands.cs deleted file mode 100644 index 518218f..0000000 --- a/src/Ellie.Bot.Modules.Searches/PathOfExileCommands.cs +++ /dev/null @@ -1,311 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Common; -using Ellie.Modules.Searches.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Globalization; -using System.Text; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class PathOfExileCommands : EllieModule - { - private const string POE_URL = "https://www.pathofexile.com/character-window/get-characters?accountName="; - private const string PON_URL = "http://poe.ninja/api/Data/GetCurrencyOverview?league="; - private const string POGS_URL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search="; - - private const string POG_URL = - "https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject="; - - private const string POGI_URL = - "https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:"; - - private const string PROFILE_URL = "https://www.pathofexile.com/account/view-profile/"; - - private readonly IHttpClientFactory _httpFactory; - - private Dictionary currencyDictionary = new(StringComparer.OrdinalIgnoreCase) - { - { "Chaos Orb", "Chaos Orb" }, - { "Orb of Alchemy", "Orb of Alchemy" }, - { "Jeweller's Orb", "Jeweller's Orb" }, - { "Exalted Orb", "Exalted Orb" }, - { "Mirror of Kalandra", "Mirror of Kalandra" }, - { "Vaal Orb", "Vaal Orb" }, - { "Orb of Alteration", "Orb of Alteration" }, - { "Orb of Scouring", "Orb of Scouring" }, - { "Divine Orb", "Divine Orb" }, - { "Orb of Annulment", "Orb of Annulment" }, - { "Master Cartographer's Sextant", "Master Cartographer's Sextant" }, - { "Journeyman Cartographer's Sextant", "Journeyman Cartographer's Sextant" }, - { "Apprentice Cartographer's Sextant", "Apprentice Cartographer's Sextant" }, - { "Blessed Orb", "Blessed Orb" }, - { "Orb of Regret", "Orb of Regret" }, - { "Gemcutter's Prism", "Gemcutter's Prism" }, - { "Glassblower's Bauble", "Glassblower's Bauble" }, - { "Orb of Fusing", "Orb of Fusing" }, - { "Cartographer's Chisel", "Cartographer's Chisel" }, - { "Chromatic Orb", "Chromatic Orb" }, - { "Orb of Augmentation", "Orb of Augmentation" }, - { "Blacksmith's Whetstone", "Blacksmith's Whetstone" }, - { "Orb of Transmutation", "Orb of Transmutation" }, - { "Armourer's Scrap", "Armourer's Scrap" }, - { "Scroll of Wisdom", "Scroll of Wisdom" }, - { "Regal Orb", "Regal Orb" }, - { "Chaos", "Chaos Orb" }, - { "Alch", "Orb of Alchemy" }, - { "Alchs", "Orb of Alchemy" }, - { "Jews", "Jeweller's Orb" }, - { "Jeweller", "Jeweller's Orb" }, - { "Jewellers", "Jeweller's Orb" }, - { "Jeweller's", "Jeweller's Orb" }, - { "X", "Exalted Orb" }, - { "Ex", "Exalted Orb" }, - { "Exalt", "Exalted Orb" }, - { "Exalts", "Exalted Orb" }, - { "Mirror", "Mirror of Kalandra" }, - { "Mirrors", "Mirror of Kalandra" }, - { "Vaal", "Vaal Orb" }, - { "Alt", "Orb of Alteration" }, - { "Alts", "Orb of Alteration" }, - { "Scour", "Orb of Scouring" }, - { "Scours", "Orb of Scouring" }, - { "Divine", "Divine Orb" }, - { "Annul", "Orb of Annulment" }, - { "Annulment", "Orb of Annulment" }, - { "Master Sextant", "Master Cartographer's Sextant" }, - { "Journeyman Sextant", "Journeyman Cartographer's Sextant" }, - { "Apprentice Sextant", "Apprentice Cartographer's Sextant" }, - { "Blessed", "Blessed Orb" }, - { "Regret", "Orb of Regret" }, - { "Regrets", "Orb of Regret" }, - { "Gcp", "Gemcutter's Prism" }, - { "Glassblowers", "Glassblower's Bauble" }, - { "Glassblower's", "Glassblower's Bauble" }, - { "Fusing", "Orb of Fusing" }, - { "Fuses", "Orb of Fusing" }, - { "Fuse", "Orb of Fusing" }, - { "Chisel", "Cartographer's Chisel" }, - { "Chisels", "Cartographer's Chisel" }, - { "Chance", "Orb of Chance" }, - { "Chances", "Orb of Chance" }, - { "Chrome", "Chromatic Orb" }, - { "Chromes", "Chromatic Orb" }, - { "Aug", "Orb of Augmentation" }, - { "Augmentation", "Orb of Augmentation" }, - { "Augment", "Orb of Augmentation" }, - { "Augments", "Orb of Augmentation" }, - { "Whetstone", "Blacksmith's Whetstone" }, - { "Whetstones", "Blacksmith's Whetstone" }, - { "Transmute", "Orb of Transmutation" }, - { "Transmutes", "Orb of Transmutation" }, - { "Armourers", "Armourer's Scrap" }, - { "Armourer's", "Armourer's Scrap" }, - { "Wisdom Scroll", "Scroll of Wisdom" }, - { "Wisdom Scrolls", "Scroll of Wisdom" }, - { "Regal", "Regal Orb" }, - { "Regals", "Regal Orb" } - }; - - public PathOfExileCommands(IHttpClientFactory httpFactory) - => _httpFactory = httpFactory; - - [Cmd] - public async Task PathOfExile(string usr, string league = "", int page = 1) - { - if (--page < 0) - return; - - if (string.IsNullOrWhiteSpace(usr)) - { - await SendErrorAsync("Please provide an account name."); - return; - } - - var characters = new List(); - - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync($"{POE_URL}{usr}"); - characters = JsonConvert.DeserializeObject>(res); - } - catch - { - var embed = _eb.Create().WithDescription(GetText(strs.account_not_found)).WithErrorColor(); - - await ctx.Channel.EmbedAsync(embed); - return; - } - - if (!string.IsNullOrWhiteSpace(league)) - characters.RemoveAll(c => c.League != league); - - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var embed = _eb.Create() - .WithAuthor($"Characters on {usr}'s account", - "https://web.poecdn.com/image/favicon/ogimage.png", - $"{PROFILE_URL}{usr}") - .WithOkColor(); - - var tempList = characters.Skip(curPage * 9).Take(9).ToList(); - - if (characters.Count == 0) - return embed.WithDescription("This account has no characters."); - - var sb = new StringBuilder(); - sb.AppendLine($"```{"#",-5}{"Character Name",-23}{"League",-10}{"Class",-13}{"Level",-3}"); - for (var i = 0; i < tempList.Count; i++) - { - var character = tempList[i]; - - sb.AppendLine( - $"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}"); - } - - sb.AppendLine("```"); - embed.WithDescription(sb.ToString()); - - return embed; - }, - characters.Count, - 9); - } - - [Cmd] - public async Task PathOfExileLeagues() - { - var leagues = new List(); - - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync("http://api.pathofexile.com/leagues?type=main&compact=1"); - leagues = JsonConvert.DeserializeObject>(res); - } - catch - { - var eembed = _eb.Create().WithDescription(GetText(strs.leagues_not_found)).WithErrorColor(); - - await ctx.Channel.EmbedAsync(eembed); - return; - } - - var embed = _eb.Create() - .WithAuthor("Path of Exile Leagues", - "https://web.poecdn.com/image/favicon/ogimage.png", - "https://www.pathofexile.com") - .WithOkColor(); - - var sb = new StringBuilder(); - sb.AppendLine($"```{"#",-5}{"League Name",-23}"); - for (var i = 0; i < leagues.Count; i++) - { - var league = leagues[i]; - - sb.AppendLine($"#{i + 1,-4}{league.Id,-23}"); - } - - sb.AppendLine("```"); - - embed.WithDescription(sb.ToString()); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - public async Task PathOfExileCurrency( - string leagueName, - string currencyName, - string convertName = "Chaos Orb") - { - if (string.IsNullOrWhiteSpace(leagueName)) - { - await SendErrorAsync("Please provide league name."); - return; - } - - if (string.IsNullOrWhiteSpace(currencyName)) - { - await SendErrorAsync("Please provide currency name."); - return; - } - - var cleanCurrency = ShortCurrencyName(currencyName); - var cleanConvert = ShortCurrencyName(convertName); - - try - { - var res = $"{PON_URL}{leagueName}"; - using var http = _httpFactory.CreateClient(); - var obj = JObject.Parse(await http.GetStringAsync(res)); - - var chaosEquivalent = 0.0F; - var conversionEquivalent = 0.0F; - - // poe.ninja API does not include a "chaosEquivalent" property for Chaos Orbs. - if (cleanCurrency == "Chaos Orb") - chaosEquivalent = 1.0F; - else - { - var currencyInput = obj["lines"] - .Values() - .Where(i => i["currencyTypeName"].Value() == cleanCurrency) - .FirstOrDefault(); - chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(), - CultureInfo.InvariantCulture); - } - - if (cleanConvert == "Chaos Orb") - conversionEquivalent = 1.0F; - else - { - var currencyOutput = obj["lines"] - .Values() - .Where(i => i["currencyTypeName"].Value() == cleanConvert) - .FirstOrDefault(); - conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(), - CultureInfo.InvariantCulture); - } - - var embed = _eb.Create() - .WithAuthor($"{leagueName} Currency Exchange", - "https://web.poecdn.com/image/favicon/ogimage.png", - "http://poe.ninja") - .AddField("Currency Type", cleanCurrency, true) - .AddField($"{cleanConvert} Equivalent", chaosEquivalent / conversionEquivalent, true) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed); - } - catch - { - var embed = _eb.Create().WithDescription(GetText(strs.ninja_not_found)).WithErrorColor(); - - await ctx.Channel.EmbedAsync(embed); - } - } - - private string ShortCurrencyName(string str) - { - if (currencyDictionary.ContainsValue(str)) - return str; - - var currency = currencyDictionary[str]; - - return currency; - } - - private static string ShortLeagueName(string str) - { - var league = str.Replace("Hardcore", "HC", StringComparison.InvariantCultureIgnoreCase); - - return league; - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/PlaceCommands.cs b/src/Ellie.Bot.Modules.Searches/PlaceCommands.cs deleted file mode 100644 index a3a2a40..0000000 --- a/src/Ellie.Bot.Modules.Searches/PlaceCommands.cs +++ /dev/null @@ -1,71 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class PlaceCommands : EllieModule - { - public enum PlaceType - { - Cage, //http://www.placecage.com - Steven, //http://www.stevensegallery.com - Beard, //http://placebeard.it - Fill, //http://www.fillmurray.com - Bear, //https://www.placebear.com - Kitten, //http://placekitten.com - Bacon, //http://baconmockup.com - Xoart //http://xoart.link - } - - private static readonly string _typesStr = string.Join(", ", Enum.GetNames()); - - [Cmd] - public async Task Placelist() - => await SendConfirmAsync(GetText(strs.list_of_place_tags(prefix)), _typesStr); - - [Cmd] - public async Task Place(PlaceType placeType, uint width = 0, uint height = 0) - { - var url = string.Empty; - switch (placeType) - { - case PlaceType.Cage: - url = "http://www.placecage.com"; - break; - case PlaceType.Steven: - url = "http://www.stevensegallery.com"; - break; - case PlaceType.Beard: - url = "http://placebeard.it"; - break; - case PlaceType.Fill: - url = "http://www.fillmurray.com"; - break; - case PlaceType.Bear: - url = "https://www.placebear.com"; - break; - case PlaceType.Kitten: - url = "http://placekitten.com"; - break; - case PlaceType.Bacon: - url = "http://baconmockup.com"; - break; - case PlaceType.Xoart: - url = "http://xoart.link"; - break; - } - - var rng = new EllieRandom(); - if (width is <= 0 or > 1000) - width = (uint)rng.Next(250, 850); - - if (height is <= 0 or > 1000) - height = (uint)rng.Next(250, 850); - - url += $"/{width}/{height}"; - - await ctx.Channel.SendMessageAsync(url); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/PokemonSearchCommands.cs b/src/Ellie.Bot.Modules.Searches/PokemonSearchCommands.cs deleted file mode 100644 index 2f25d85..0000000 --- a/src/Ellie.Bot.Modules.Searches/PokemonSearchCommands.cs +++ /dev/null @@ -1,74 +0,0 @@ -#nullable disable -using Ellie.Modules.Searches.Services; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class PokemonSearchCommands : EllieModule - { - private readonly ILocalDataCache _cache; - - public PokemonSearchCommands(ILocalDataCache cache) - => _cache = cache; - - [Cmd] - public async Task Pokemon([Leftover] string pokemon = null) - { - pokemon = pokemon?.Trim().ToUpperInvariant(); - if (string.IsNullOrWhiteSpace(pokemon)) - return; - - foreach (var kvp in await _cache.GetPokemonsAsync()) - { - if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant()) - { - var p = kvp.Value; - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(kvp.Key.ToTitleCase()) - .WithDescription(p.BaseStats.ToString()) - .WithThumbnailUrl( - $"https://assets.pokemon.com/assets/cms2/img/pokedex/detail/{p.Id.ToString("000")}.png") - .AddField(GetText(strs.types), string.Join("\n", p.Types), true) - .AddField(GetText(strs.height_weight), - GetText(strs.height_weight_val(p.HeightM, p.WeightKg)), - true) - .AddField(GetText(strs.abilities), - string.Join("\n", p.Abilities.Select(a => a.Value)), - true)); - return; - } - } - - await ReplyErrorLocalizedAsync(strs.pokemon_none); - } - - [Cmd] - public async Task PokemonAbility([Leftover] string ability = null) - { - ability = ability?.Trim().ToUpperInvariant().Replace(" ", "", StringComparison.InvariantCulture); - if (string.IsNullOrWhiteSpace(ability)) - return; - foreach (var kvp in await _cache.GetPokemonAbilitiesAsync()) - { - if (kvp.Key.ToUpperInvariant() == ability) - { - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(kvp.Value.Name) - .WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc) - ? kvp.Value.ShortDesc - : kvp.Value.Desc) - .AddField(GetText(strs.rating), - kvp.Value.Rating.ToString(Culture), - true)); - return; - } - } - - await ReplyErrorLocalizedAsync(strs.pokemon_ability_none); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Searches.cs b/src/Ellie.Bot.Modules.Searches/Searches.cs deleted file mode 100644 index 0970636..0000000 --- a/src/Ellie.Bot.Modules.Searches/Searches.cs +++ /dev/null @@ -1,612 +0,0 @@ -#nullable disable -using Microsoft.Extensions.Caching.Memory; -using Ellie.Modules.Searches.Common; -using Ellie.Modules.Searches.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -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; - -namespace Ellie.Modules.Searches; - -public partial class Searches : EllieModule -{ - private static readonly ConcurrentDictionary _cachedShortenedLinks = new(); - private readonly IBotCredentials _creds; - private readonly IGoogleApiService _google; - private readonly IHttpClientFactory _httpFactory; - private readonly IMemoryCache _cache; - private readonly ITimezoneService _tzSvc; - - public Searches( - IBotCredentials creds, - IGoogleApiService google, - IHttpClientFactory factory, - IMemoryCache cache, - ITimezoneService tzSvc) - { - _creds = creds; - _google = google; - _httpFactory = factory; - _cache = cache; - _tzSvc = tzSvc; - } - - [Cmd] - public async Task Rip([Leftover] IGuildUser usr) - { - var av = usr.RealAvatarUrl(); - await using var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av); - await ctx.Channel.SendFileAsync(picStream, - "rip.png", - $"Rip {Format.Bold(usr.ToString())} \n\t- " + Format.Italics(ctx.User.ToString())); - } - - [Cmd] - public async Task Weather([Leftover] string query) - { - if (!await ValidateQuery(query)) - return; - - var embed = _eb.Create(); - var data = await _service.GetWeatherDataAsync(query); - - if (data is null) - embed.WithDescription(GetText(strs.city_not_found)).WithErrorColor(); - else - { - var f = StandardConversions.CelsiusToFahrenheit; - - var tz = _tzSvc.GetTimeZoneOrUtc(ctx.Guild?.Id); - var sunrise = data.Sys.Sunrise.ToUnixTimestamp(); - var sunset = data.Sys.Sunset.ToUnixTimestamp(); - sunrise = sunrise.ToOffset(tz.GetUtcOffset(sunrise)); - sunset = sunset.ToOffset(tz.GetUtcOffset(sunset)); - var timezone = $"UTC{sunrise:zzz}"; - - embed - .AddField("🌍 " + Format.Bold(GetText(strs.location)), - $"[{data.Name + ", " + data.Sys.Country}](https://openweathermap.org/city/{data.Id})", - true) - .AddField("📏 " + Format.Bold(GetText(strs.latlong)), $"{data.Coord.Lat}, {data.Coord.Lon}", true) - .AddField("☁ " + Format.Bold(GetText(strs.condition)), - string.Join(", ", data.Weather.Select(w => w.Main)), - true) - .AddField("😓 " + Format.Bold(GetText(strs.humidity)), $"{data.Main.Humidity}%", true) - .AddField("💨 " + Format.Bold(GetText(strs.wind_speed)), data.Wind.Speed + " m/s", true) - .AddField("🌡 " + Format.Bold(GetText(strs.temperature)), - $"{data.Main.Temp:F1}°C / {f(data.Main.Temp):F1}°F", - true) - .AddField("🔆 " + Format.Bold(GetText(strs.min_max)), - $"{data.Main.TempMin:F1}°C - {data.Main.TempMax:F1}°C\n{f(data.Main.TempMin):F1}°F - {f(data.Main.TempMax):F1}°F", - true) - .AddField("🌄 " + Format.Bold(GetText(strs.sunrise)), $"{sunrise:HH:mm} {timezone}", true) - .AddField("🌇 " + Format.Bold(GetText(strs.sunset)), $"{sunset:HH:mm} {timezone}", true) - .WithOkColor() - .WithFooter("Powered by openweathermap.org", - $"https://openweathermap.org/img/w/{data.Weather[0].Icon}.png"); - } - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - public async Task Time([Leftover] string query) - { - if (!await ValidateQuery(query)) - return; - - await ctx.Channel.TriggerTypingAsync(); - - var (data, err) = await _service.GetTimeDataAsync(query); - if (err is not null) - { - LocStr errorKey; - switch (err) - { - case TimeErrors.ApiKeyMissing: - errorKey = strs.api_key_missing; - break; - case TimeErrors.InvalidInput: - errorKey = strs.invalid_input; - break; - case TimeErrors.NotFound: - errorKey = strs.not_found; - break; - default: - errorKey = strs.error_occured; - break; - } - - await ReplyErrorLocalizedAsync(errorKey); - return; - } - - if (string.IsNullOrWhiteSpace(data.TimeZoneName)) - { - await ReplyErrorLocalizedAsync(strs.timezone_db_api_key); - return; - } - - var eb = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.time_new)) - .WithDescription(Format.Code(data.Time.ToString(Culture))) - .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) - .AddField(GetText(strs.timezone), data.TimeZoneName, true); - - await ctx.Channel.SendMessageAsync(embed: eb.Build()); - } - - [Cmd] - public async Task Movie([Leftover] string query = null) - { - if (!await ValidateQuery(query)) - return; - - await ctx.Channel.TriggerTypingAsync(); - - var movie = await _service.GetMovieDataAsync(query); - if (movie is null) - { - await ReplyErrorLocalizedAsync(strs.imdb_fail); - return; - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(movie.Title) - .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") - .WithDescription(movie.Plot.TrimTo(1000)) - .AddField("Rating", movie.ImdbRating, true) - .AddField("Genre", movie.Genre, true) - .AddField("Year", movie.Year, true) - .WithImageUrl(movie.Poster)); - } - - [Cmd] - public Task RandomCat() - => InternalRandomImage(SearchesService.ImageTag.Cats); - - [Cmd] - public Task RandomDog() - => InternalRandomImage(SearchesService.ImageTag.Dogs); - - [Cmd] - public Task RandomFood() - => InternalRandomImage(SearchesService.ImageTag.Food); - - [Cmd] - public Task RandomBird() - => InternalRandomImage(SearchesService.ImageTag.Birds); - - private Task InternalRandomImage(SearchesService.ImageTag tag) - { - var url = _service.GetRandomImageUrl(tag); - return ctx.Channel.EmbedAsync(_eb.Create().WithOkColor().WithImageUrl(url)); - } - - [Cmd] - public async Task Lmgtfy([Leftover] string ffs = null) - { - if (!await ValidateQuery(ffs)) - return; - - var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(ffs)}"); - await SendConfirmAsync($"<{shortenedUrl}>"); - } - - [Cmd] - public async Task Shorten([Leftover] string query) - { - if (!await ValidateQuery(query)) - return; - - query = query.Trim(); - if (!_cachedShortenedLinks.TryGetValue(query, out var shortLink)) - { - try - { - using var http = _httpFactory.CreateClient(); - using var req = new HttpRequestMessage(HttpMethod.Post, "https://goolnk.com/api/v1/shorten"); - var formData = new MultipartFormDataContent - { - { new StringContent(query), "url" } - }; - req.Content = formData; - - using var res = await http.SendAsync(req); - var content = await res.Content.ReadAsStringAsync(); - var data = JsonConvert.DeserializeObject(content); - - if (!string.IsNullOrWhiteSpace(data?.ResultUrl)) - _cachedShortenedLinks.TryAdd(query, data.ResultUrl); - else - return; - - shortLink = data.ResultUrl; - } - catch (Exception ex) - { - Log.Error(ex, "Error shortening a link: {Message}", ex.Message); - return; - } - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .AddField(GetText(strs.original_url), $"<{query}>") - .AddField(GetText(strs.short_url), $"<{shortLink}>")); - } - - [Cmd] - public async Task MagicTheGathering([Leftover] string search) - { - if (!await ValidateQuery(search)) - return; - - await ctx.Channel.TriggerTypingAsync(); - var card = await _service.GetMtgCardAsync(search); - - if (card is null) - { - await ReplyErrorLocalizedAsync(strs.card_not_found); - return; - } - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(card.Name) - .WithDescription(card.Description) - .WithImageUrl(card.ImageUrl) - .AddField(GetText(strs.store_url), card.StoreUrl, true) - .AddField(GetText(strs.cost), card.ManaCost, true) - .AddField(GetText(strs.types), card.Types, true); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - public async Task Hearthstone([Leftover] string name) - { - if (!await ValidateQuery(name)) - return; - - if (string.IsNullOrWhiteSpace(_creds.RapidApiKey)) - { - await ReplyErrorLocalizedAsync(strs.mashape_api_missing); - return; - } - - await ctx.Channel.TriggerTypingAsync(); - var card = await _service.GetHearthstoneCardDataAsync(name); - - if (card is null) - { - await ReplyErrorLocalizedAsync(strs.card_not_found); - return; - } - - var embed = _eb.Create().WithOkColor().WithImageUrl(card.Img); - - if (!string.IsNullOrWhiteSpace(card.Flavor)) - embed.WithDescription(card.Flavor); - - await ctx.Channel.EmbedAsync(embed); - } - - [Cmd] - public async Task UrbanDict([Leftover] string query = null) - { - if (!await ValidateQuery(query)) - return; - - await ctx.Channel.TriggerTypingAsync(); - using (var http = _httpFactory.CreateClient()) - { - var res = await http.GetStringAsync( - $"https://api.urbandictionary.com/v0/define?term={Uri.EscapeDataString(query)}"); - try - { - var items = JsonConvert.DeserializeObject(res).List; - if (items.Any()) - { - await ctx.SendPaginatedConfirmAsync(0, - p => - { - var item = items[p]; - return _eb.Create() - .WithOkColor() - .WithUrl(item.Permalink) - .WithTitle(item.Word) - .WithDescription(item.Definition); - }, - items.Length, - 1); - return; - } - } - catch - { - } - } - - await ReplyErrorLocalizedAsync(strs.ud_error); - } - - [Cmd] - public async Task Define([Leftover] string word) - { - if (!await ValidateQuery(word)) - return; - - using var http = _httpFactory.CreateClient(); - string res; - try - { - res = await _cache.GetOrCreateAsync($"define_{word}", - e => - { - e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12); - return http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword=" - + WebUtility.UrlEncode(word)); - }); - - var data = JsonConvert.DeserializeObject(res); - - var datas = data.Results - .Where(x => x.Senses is not null - && x.Senses.Count > 0 - && x.Senses[0].Definition is not null) - .Select(x => (Sense: x.Senses[0], x.PartOfSpeech)) - .ToList(); - - if (!datas.Any()) - { - Log.Warning("Definition not found: {Word}", word); - await ReplyErrorLocalizedAsync(strs.define_unknown); - } - - - var col = datas.Select(x => ( - Definition: x.Sense.Definition is string - ? x.Sense.Definition.ToString() - : ((JArray)JToken.Parse(x.Sense.Definition.ToString())).First.ToString(), - Example: x.Sense.Examples is null || x.Sense.Examples.Count == 0 - ? string.Empty - : x.Sense.Examples[0].Text, Word: word, - WordType: string.IsNullOrWhiteSpace(x.PartOfSpeech) ? "-" : x.PartOfSpeech)) - .ToList(); - - Log.Information("Sending {Count} definition for: {Word}", col.Count, word); - - await ctx.SendPaginatedConfirmAsync(0, - page => - { - var model = col.Skip(page).First(); - var embed = _eb.Create() - .WithDescription(ctx.User.Mention) - .AddField(GetText(strs.word), model.Word, true) - .AddField(GetText(strs._class), model.WordType, true) - .AddField(GetText(strs.definition), model.Definition) - .WithOkColor(); - - if (!string.IsNullOrWhiteSpace(model.Example)) - embed.AddField(GetText(strs.example), model.Example); - - return embed; - }, - col.Count, - 1); - } - catch (Exception ex) - { - Log.Error(ex, "Error retrieving definition data for: {Word}", word); - } - } - - [Cmd] - public async Task Catfact() - { - using var http = _httpFactory.CreateClient(); - var response = await http.GetStringAsync("https://catfact.ninja/fact"); - - var fact = JObject.Parse(response)["fact"].ToString(); - await SendConfirmAsync("🐈" + GetText(strs.catfact), fact); - } - - //done in 3.0 - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Revav([Leftover] IGuildUser usr = null) - { - if (usr is null) - usr = (IGuildUser)ctx.User; - - var av = usr.RealAvatarUrl(); - await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={av}"); - } - - //done in 3.0 - [Cmd] - public async Task Revimg([Leftover] string imageLink = null) - { - imageLink = imageLink?.Trim() ?? ""; - - if (string.IsNullOrWhiteSpace(imageLink)) - return; - - await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}"); - } - - [Cmd] - public async Task Wiki([Leftover] string query = null) - { - query = query?.Trim(); - - if (!await ValidateQuery(query)) - return; - - using var http = _httpFactory.CreateClient(); - var result = await http.GetStringAsync( - "https://en.wikipedia.org//w/api.php?action=query&format=json&prop=info&redirects=1&formatversion=2&inprop=url&titles=" - + Uri.EscapeDataString(query)); - var data = JsonConvert.DeserializeObject(result); - if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl)) - await ReplyErrorLocalizedAsync(strs.wiki_page_not_found); - else - await ctx.Channel.SendMessageAsync(data.Query.Pages[0].FullUrl); - } - - [Cmd] - public async Task Color(params Color[] colors) - { - if (!colors.Any()) - return; - - var colorObjects = colors.Take(10).ToArray(); - - using var img = new Image(colorObjects.Length * 50, 50); - for (var i = 0; i < colorObjects.Length; i++) - { - var x = i * 50; - img.Mutate(m => m.FillPolygon(colorObjects[i], new(x, 0), new(x + 50, 0), new(x + 50, 50), new(x, 50))); - } - - await using var ms = img.ToStream(); - await ctx.Channel.SendFileAsync(ms, "colors.png"); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Avatar([Leftover] IGuildUser usr = null) - { - if (usr is null) - usr = (IGuildUser)ctx.User; - - var avatarUrl = usr.RealAvatarUrl(2048); - - await ctx.Channel.EmbedAsync( - _eb.Create() - .WithOkColor() - .AddField("Username", usr.ToString()) - .AddField("Avatar Url", avatarUrl) - .WithThumbnailUrl(avatarUrl.ToString()), - ctx.User.Mention); - } - - [Cmd] - public async Task Wikia(string target, [Leftover] string query) - { - if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query)) - { - await ReplyErrorLocalizedAsync(strs.wikia_input_error); - return; - } - - await ctx.Channel.TriggerTypingAsync(); - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Clear(); - try - { - var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php" - + "?action=query" - + "&format=json" - + "&list=search" - + $"&srsearch={Uri.EscapeDataString(query)}" - + "&srlimit=1"); - var items = JObject.Parse(res); - var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString(); - - if (string.IsNullOrWhiteSpace(title)) - { - await ReplyErrorLocalizedAsync(strs.wikia_error); - return; - } - - var url = Uri.EscapeDataString($"https://{target}.fandom.com/wiki/{title}"); - var response = $@"`{GetText(strs.title)}` {title.SanitizeMentions()} -`{GetText(strs.url)}:` {url}"; - await ctx.Channel.SendMessageAsync(response); - } - catch - { - await ReplyErrorLocalizedAsync(strs.wikia_error); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Bible(string book, string chapterAndVerse) - { - var obj = new BibleVerses(); - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync($"https://bible-api.com/{book} {chapterAndVerse}"); - - obj = JsonConvert.DeserializeObject(res); - } - catch - { - } - - if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0) - await SendErrorAsync(obj.Error ?? "No verse found."); - else - { - var v = obj.Verses[0]; - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}") - .WithDescription(v.Text)); - } - } - - [Cmd] - public async Task Steam([Leftover] string query) - { - if (string.IsNullOrWhiteSpace(query)) - return; - - await ctx.Channel.TriggerTypingAsync(); - - var appId = await _service.GetSteamAppIdByName(query); - if (appId == -1) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - //var embed = _eb.Create() - // .WithOkColor() - // .WithDescription(gameData.ShortDescription) - // .WithTitle(gameData.Name) - // .WithUrl(gameData.Link) - // .WithImageUrl(gameData.HeaderImage) - // .AddField(GetText(strs.genres), gameData.TotalEpisodes.ToString(), true) - // .AddField(GetText(strs.price), gameData.IsFree ? GetText(strs.FREE) : game, true) - // .AddField(GetText(strs.links), gameData.GetGenresString(), true) - // .WithFooter(GetText(strs.recommendations(gameData.TotalRecommendations))); - await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}"); - } - - private async Task ValidateQuery([MaybeNullWhen(false)] string query) - { - if (!string.IsNullOrWhiteSpace(query)) - return true; - - await ErrorLocalizedAsync(strs.specify_search_params); - return false; - } - - public class ShortenData - { - [JsonProperty("result_url")] public string ResultUrl { get; set; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/SearchesService.cs b/src/Ellie.Bot.Modules.Searches/SearchesService.cs deleted file mode 100644 index 71477ee..0000000 --- a/src/Ellie.Bot.Modules.Searches/SearchesService.cs +++ /dev/null @@ -1,468 +0,0 @@ -#nullable disable -using Html2Markdown; -using Ellie.Modules.Searches.Common; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Color = SixLabors.ImageSharp.Color; -using Image = SixLabors.ImageSharp.Image; - -namespace Ellie.Modules.Searches.Services; - -public class SearchesService : IEService -{ - public enum ImageTag - { - Food, - Dogs, - Cats, - Birds - } - - public List WowJokes { get; } = new(); - public List MagicItems { get; } = new(); - private readonly IHttpClientFactory _httpFactory; - private readonly IGoogleApiService _google; - private readonly IImageCache _imgs; - private readonly IBotCache _c; - private readonly FontProvider _fonts; - private readonly IBotCredsProvider _creds; - private readonly EllieRandom _rng; - private readonly List _yomamaJokes; - - private readonly object _yomamaLock = new(); - private int yomamaJokeIndex; - - public SearchesService( - IGoogleApiService google, - IImageCache images, - IBotCache c, - IHttpClientFactory factory, - FontProvider fonts, - IBotCredsProvider creds) - { - _httpFactory = factory; - _google = google; - _imgs = images; - _c = c; - _fonts = fonts; - _creds = creds; - _rng = new(); - - //joke commands - if (File.Exists("data/wowjokes.json")) - WowJokes = JsonConvert.DeserializeObject>(File.ReadAllText("data/wowjokes.json")); - else - Log.Warning("data/wowjokes.json is missing. WOW Jokes are not loaded"); - - if (File.Exists("data/magicitems.json")) - MagicItems = JsonConvert.DeserializeObject>(File.ReadAllText("data/magicitems.json")); - else - Log.Warning("data/magicitems.json is missing. Magic items are not loaded"); - - if (File.Exists("data/yomama.txt")) - _yomamaJokes = File.ReadAllLines("data/yomama.txt").Shuffle().ToList(); - else - { - _yomamaJokes = new(); - Log.Warning("data/yomama.txt is missing. .yomama command won't work"); - } - } - - public async Task GetRipPictureAsync(string text, Uri imgUrl) - => (await GetRipPictureFactory(text, imgUrl)).ToStream(); - - private void DrawAvatar(Image bg, Image avatarImage) - => bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions())); - - public async Task GetRipPictureFactory(string text, Uri avatarUrl) - { - using var bg = Image.Load(await _imgs.GetRipBgAsync()); - var result = await _c.GetImageDataAsync(avatarUrl); - if (!result.TryPickT0(out var data, out _)) - { - using var http = _httpFactory.CreateClient(); - data = await http.GetByteArrayAsync(avatarUrl); - using (var avatarImg = Image.Load(data)) - { - avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42)); - await using var avStream = await avatarImg.ToStreamAsync(); - data = avStream.ToArray(); - DrawAvatar(bg, avatarImg); - } - - await _c.SetImageDataAsync(avatarUrl, data); - } - else - { - using var avatarImg = Image.Load(data); - DrawAvatar(bg, avatarImg); - } - - bg.Mutate(x => x.DrawText( - new TextOptions(_fonts.RipFont) - { - HorizontalAlignment = HorizontalAlignment.Center, - FallbackFontFamilies = _fonts.FallBackFonts, - Origin = new(bg.Width / 2, 225), - }, - text, - Color.Black)); - - //flowa - using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync())) - { - bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions())); - } - - await using var stream = bg.ToStream(); - return stream.ToArray(); - } - - public async Task GetWeatherDataAsync(string query) - { - query = query.Trim().ToLowerInvariant(); - - return await _c.GetOrAddAsync(new($"nadeko_weather_{query}"), - async () => await GetWeatherDataFactory(query), - TimeSpan.FromHours(3)); - } - - private async Task GetWeatherDataFactory(string query) - { - using var http = _httpFactory.CreateClient(); - try - { - var data = await http.GetStringAsync("https://api.openweathermap.org/data/2.5/weather?" - + $"q={query}&" - + "appid=42cd627dd60debf25a5739e50a217d74&" - + "units=metric"); - - if (string.IsNullOrWhiteSpace(data)) - return null; - - return JsonConvert.DeserializeObject(data); - } - catch (Exception ex) - { - Log.Warning(ex, "Error getting weather data"); - return null; - } - } - - public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg) - => GetTimeDataFactory(arg); - - //return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}", - // GetTimeDataFactory, - // arg, - // TimeSpan.FromMinutes(1)); - private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory( - string query) - { - query = query.Trim(); - - if (string.IsNullOrEmpty(query)) - return (default, TimeErrors.InvalidInput); - - - var locIqKey = _creds.GetCreds().LocationIqApiKey; - var tzDbKey = _creds.GetCreds().TimezoneDbApiKey; - if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey)) - return (default, TimeErrors.ApiKeyMissing); - - try - { - using var http = _httpFactory.CreateClient(); - var res = await _c.GetOrAddAsync(new($"searches:geo:{query}"), - async () => - { - var url = "https://eu1.locationiq.com/v1/search.php?" - + (string.IsNullOrWhiteSpace(locIqKey) - ? "key=" - : $"key={locIqKey}&") - + $"q={Uri.EscapeDataString(query)}&" - + "format=json"; - - var res = await http.GetStringAsync(url); - return res; - }, - TimeSpan.FromHours(1)); - - var responses = JsonConvert.DeserializeObject(res); - if (responses is null || responses.Length == 0) - { - Log.Warning("Geocode lookup failed for: {Query}", query); - return (default, TimeErrors.NotFound); - } - - var geoData = responses[0]; - - using var req = new HttpRequestMessage(HttpMethod.Get, - "http://api.timezonedb.com/v2.1/get-time-zone?" - + $"key={tzDbKey}" - + $"&format=json" - + $"&by=position" - + $"&lat={geoData.Lat}" - + $"&lng={geoData.Lon}"); - - using var geoRes = await http.SendAsync(req); - var resString = await geoRes.Content.ReadAsStringAsync(); - var timeObj = JsonConvert.DeserializeObject(resString); - - var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timeObj.Timestamp); - - return ((Address: responses[0].DisplayName, Time: time, TimeZoneName: timeObj.TimezoneName), default); - } - catch (Exception ex) - { - Log.Error(ex, "Weather error: {Message}", ex.Message); - return (default, TimeErrors.NotFound); - } - } - - public string GetRandomImageUrl(ImageTag tag) - { - var subpath = tag.ToString().ToLowerInvariant(); - - int max; - switch (tag) - { - case ImageTag.Food: - max = 773; - break; - case ImageTag.Dogs: - max = 750; - break; - case ImageTag.Cats: - max = 773; - break; - case ImageTag.Birds: - max = 578; - break; - default: - max = 100; - break; - } - - return $"https://nadeko-pictures.nyc3.digitaloceanspaces.com/{subpath}/" - + _rng.Next(1, max).ToString("000") - + ".png"; - } - - public Task GetYomamaJoke() - { - string joke; - lock (_yomamaLock) - { - if (yomamaJokeIndex >= _yomamaJokes.Count) - { - yomamaJokeIndex = 0; - var newList = _yomamaJokes.ToList(); - _yomamaJokes.Clear(); - _yomamaJokes.AddRange(newList.Shuffle()); - } - - joke = _yomamaJokes[yomamaJokeIndex++]; - } - - return Task.FromResult(joke); - - // using (var http = _httpFactory.CreateClient()) - // { - // var response = await http.GetStringAsync(new Uri("http://api.yomomma.info/")); - // return JObject.Parse(response)["joke"].ToString() + " 😆"; - // } - } - - public async Task<(string Setup, string Punchline)> GetRandomJoke() - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync("https://official-joke-api.appspot.com/random_joke"); - var resObj = JsonConvert.DeserializeAnonymousType(res, - new - { - setup = "", - punchline = "" - }); - return (resObj.setup, resObj.punchline); - } - - public async Task GetChuckNorrisJoke() - { - using var http = _httpFactory.CreateClient(); - var response = await http.GetStringAsync(new Uri("https://api.chucknorris.io/jokes/random")); - return JObject.Parse(response)["value"] + " 😆"; - } - - public async Task GetMtgCardAsync(string search) - { - search = search.Trim().ToLowerInvariant(); - var data = await _c.GetOrAddAsync(new($"mtg:{search}"), - async () => await GetMtgCardFactory(search), - TimeSpan.FromDays(1)); - - if (data is null || data.Length == 0) - return null; - - return data[_rng.Next(0, data.Length)]; - } - - private async Task GetMtgCardFactory(string search) - { - async Task GetMtgDataAsync(MtgResponse.Data card) - { - string storeUrl; - try - { - storeUrl = await _google.ShortenUrl("https://shop.tcgplayer.com/productcatalog/product/show?" - + "newSearch=false&" - + "ProductType=All&" - + "IsProductNameExact=false&" - + $"ProductName={Uri.EscapeDataString(card.Name)}"); - } - catch { storeUrl = ""; } - - return new() - { - Description = card.Text, - Name = card.Name, - ImageUrl = card.ImageUrl, - StoreUrl = storeUrl, - Types = string.Join(",\n", card.Types), - ManaCost = card.ManaCost - }; - } - - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Clear(); - var response = - await http.GetStringAsync($"https://api.magicthegathering.io/v1/cards?name={Uri.EscapeDataString(search)}"); - - var responseObject = JsonConvert.DeserializeObject(response); - if (responseObject is null) - return Array.Empty(); - - var cards = responseObject.Cards.Take(5).ToArray(); - if (cards.Length == 0) - return Array.Empty(); - - return await cards.Select(GetMtgDataAsync).WhenAll(); - } - - public async Task GetHearthstoneCardDataAsync(string name) - { - name = name.ToLowerInvariant(); - return await _c.GetOrAddAsync($"hearthstone:{name}", - () => HearthstoneCardDataFactory(name), - TimeSpan.FromDays(1)); - } - - private async Task HearthstoneCardDataFactory(string name) - { - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.GetCreds().RapidApiKey); - try - { - var response = await http.GetStringAsync("https://omgvamp-hearthstone-v1.p.rapidapi.com/" - + $"cards/search/{Uri.EscapeDataString(name)}"); - var objs = JsonConvert.DeserializeObject(response); - if (objs is null || objs.Length == 0) - return null; - var data = objs.FirstOrDefault(x => x.Collectible) - ?? objs.FirstOrDefault(x => !string.IsNullOrEmpty(x.PlayerClass)) ?? objs.FirstOrDefault(); - if (data is null) - return null; - if (!string.IsNullOrWhiteSpace(data.Img)) - data.Img = await _google.ShortenUrl(data.Img); - if (!string.IsNullOrWhiteSpace(data.Text)) - { - var converter = new Converter(); - data.Text = converter.Convert(data.Text); - } - - return data; - } - catch (Exception ex) - { - Log.Error(ex, "Error getting Hearthstone Card: {ErrorMessage}", ex.Message); - return null; - } - } - - public async Task GetMovieDataAsync(string name) - { - name = name.Trim().ToLowerInvariant(); - return await _c.GetOrAddAsync(new($"movie:{name}"), - () => GetMovieDataFactory(name), - TimeSpan.FromDays(1)); - } - - private async Task GetMovieDataFactory(string name) - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/" - + "?t={0}" - + "&y=" - + "&plot=full" - + "&r=json", - name.Trim().Replace(' ', '+'))); - var movie = JsonConvert.DeserializeObject(res); - if (movie?.Title is null) - return null; - movie.Poster = await _google.ShortenUrl(movie.Poster); - return movie; - } - - public async Task GetSteamAppIdByName(string query) - { - const string steamGameIdsKey = "steam_names_to_appid"; - - var gamesMap = await _c.GetOrAddAsync(new(steamGameIdsKey), - async () => - { - 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; - - return apps.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase) - .GroupBy(x => x.Name) - .ToDictionary(x => x.Key, x => x.First().AppId); - }, - TimeSpan.FromHours(24)); - - if (gamesMap is null) - return -1; - - query = query.Trim(); - - var keyList = gamesMap.Keys.ToList(); - - var key = keyList.FirstOrDefault(x => x.Equals(query, StringComparison.OrdinalIgnoreCase)); - - if (key == default) - { - key = keyList.FirstOrDefault(x => x.StartsWith(query, StringComparison.OrdinalIgnoreCase)); - if (key == default) - return -1; - } - - return gamesMap[key]; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationCommands.cs b/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationCommands.cs deleted file mode 100644 index cacffee..0000000 --- a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationCommands.cs +++ /dev/null @@ -1,200 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Searches.Services; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class StreamNotificationCommands : EllieModule - { - private readonly DbService _db; - - public StreamNotificationCommands(DbService db) - => _db = db; - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task StreamAdd(string link) - { - var data = await _service.FollowStream(ctx.Guild.Id, ctx.Channel.Id, link); - if (data is null) - { - await ReplyErrorLocalizedAsync(strs.stream_not_added); - return; - } - - var embed = _service.GetEmbed(ctx.Guild.Id, data); - await ctx.Channel.EmbedAsync(embed, GetText(strs.stream_tracked)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [Priority(1)] - public async Task StreamRemove(int index) - { - if (--index < 0) - return; - - var fs = await _service.UnfollowStreamAsync(ctx.Guild.Id, index); - if (fs is null) - { - await ReplyErrorLocalizedAsync(strs.stream_no); - return; - } - - await ReplyConfirmLocalizedAsync(strs.stream_removed(Format.Bold(fs.Username), fs.Type)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task StreamsClear() - { - await _service.ClearAllStreams(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.streams_cleared); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task StreamList(int page = 1) - { - if (page-- < 1) - return; - - var streams = new List(); - await using (var uow = _db.GetDbContext()) - { - var all = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(gc => gc.FollowedStreams)) - .FollowedStreams.OrderBy(x => x.Id) - .ToList(); - - for (var index = all.Count - 1; index >= 0; index--) - { - var fs = all[index]; - if (((SocketGuild)ctx.Guild).GetTextChannel(fs.ChannelId) is null) - await _service.UnfollowStreamAsync(fs.GuildId, index); - else - streams.Insert(0, fs); - } - } - - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var elements = streams - .Skip(cur * 12) - .Take(12) - .ToList(); - - if (elements.Count == 0) - return _eb.Create().WithDescription(GetText(strs.streams_none)).WithErrorColor(); - - var eb = _eb.Create().WithTitle(GetText(strs.streams_follow_title)).WithOkColor(); - for (var index = 0; index < elements.Count; index++) - { - var elem = elements[index]; - eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}", - $"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}", - true); - } - - return eb; - }, - streams.Count, - 12); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task StreamOffline() - { - var newValue = _service.ToggleStreamOffline(ctx.Guild.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.stream_off_enabled); - else - await ReplyConfirmLocalizedAsync(strs.stream_off_disabled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task StreamOnlineDelete() - { - var newValue = _service.ToggleStreamOnlineDelete(ctx.Guild.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.stream_online_delete_enabled); - else - await ReplyConfirmLocalizedAsync(strs.stream_online_delete_disabled); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task StreamMessage(int index, [Leftover] string message) - { - if (--index < 0) - return; - - if (!_service.SetStreamMessage(ctx.Guild.Id, index, message, out var fs)) - { - await ReplyConfirmLocalizedAsync(strs.stream_not_following); - return; - } - - if (string.IsNullOrWhiteSpace(message)) - await ReplyConfirmLocalizedAsync(strs.stream_message_reset(Format.Bold(fs.Username))); - else - await ReplyConfirmLocalizedAsync(strs.stream_message_set(Format.Bold(fs.Username))); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task StreamMessageAll([Leftover] string message) - { - var count = _service.SetStreamMessageForAll(ctx.Guild.Id, message); - - if (count == 0) - { - await ReplyConfirmLocalizedAsync(strs.stream_not_following_any); - return; - } - - await ReplyConfirmLocalizedAsync(strs.stream_message_set_all(count)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task StreamCheck(string url) - { - try - { - var data = await _service.GetStreamDataAsync(url); - if (data is null) - { - await ReplyErrorLocalizedAsync(strs.no_channel_found); - return; - } - - if (data.IsLive) - { - await ReplyConfirmLocalizedAsync(strs.streamer_online(Format.Bold(data.Name), - Format.Bold(data.Viewers.ToString()))); - } - else - await ReplyConfirmLocalizedAsync(strs.streamer_offline(data.Name)); - } - catch - { - await ReplyErrorLocalizedAsync(strs.no_channel_found); - } - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs b/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs deleted file mode 100644 index e4a4e42..0000000 --- a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs +++ /dev/null @@ -1,617 +0,0 @@ -#nullable disable -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db; -using Ellie.Db.Models; -using Ellie.Modules.Searches.Common; -using Ellie.Modules.Searches.Common.StreamNotifications; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Searches.Services; - -public sealed class StreamNotificationService : IEService, IReadyExecutor -{ - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly Random _rng = new EllieRandom(); - private readonly DiscordSocketClient _client; - private readonly NotifChecker _streamTracker; - - private readonly object _shardLock = new(); - - private readonly Dictionary> _trackCounter = new(); - - private readonly Dictionary>> _shardTrackedStreams; - private readonly ConcurrentHashSet _offlineNotificationServers; - private readonly ConcurrentHashSet _deleteOnOfflineServers; - - private readonly IPubSub _pubSub; - private readonly IEmbedBuilderService _eb; - - public TypedKey> StreamsOnlineKey { get; } - public TypedKey> StreamsOfflineKey { get; } - - private readonly TypedKey _streamFollowKey; - private readonly TypedKey _streamUnfollowKey; - - public event Func< - FollowedStream.FType, - string, - IReadOnlyCollection<(ulong, ulong)>, - Task> OnlineMessagesSent = static delegate { return Task.CompletedTask; }; - - public StreamNotificationService( - DbService db, - DiscordSocketClient client, - IBotStrings strings, - IBotCredsProvider creds, - IHttpClientFactory httpFactory, - IBot bot, - IPubSub pubSub, - IEmbedBuilderService eb) - { - _db = db; - _client = client; - _strings = strings; - _pubSub = pubSub; - _eb = eb; - - _streamTracker = new(httpFactory, creds); - - StreamsOnlineKey = new("streams.online"); - StreamsOfflineKey = new("streams.offline"); - - _streamFollowKey = new("stream.follow"); - _streamUnfollowKey = new("stream.unfollow"); - - using (var uow = db.GetDbContext()) - { - var ids = client.GetGuildIds(); - var guildConfigs = uow.Set() - .AsQueryable() - .Include(x => x.FollowedStreams) - .Where(x => ids.Contains(x.GuildId)) - .ToList(); - - _offlineNotificationServers = new(guildConfigs - .Where(gc => gc.NotifyStreamOffline) - .Select(x => x.GuildId) - .ToList()); - - _deleteOnOfflineServers = new(guildConfigs - .Where(gc => gc.DeleteStreamOnlineMessage) - .Select(x => x.GuildId) - .ToList()); - - var followedStreams = guildConfigs.SelectMany(x => x.FollowedStreams).ToList(); - - _shardTrackedStreams = followedStreams.GroupBy(x => new - { - x.Type, - Name = x.Username.ToLower() - }) - .ToList() - .ToDictionary( - x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()), - x => x.GroupBy(y => y.GuildId) - .ToDictionary(y => y.Key, - y => y.AsEnumerable().ToHashSet())); - - // shard 0 will keep track of when there are no more guilds which track a stream - if (client.ShardId == 0) - { - var allFollowedStreams = uow.Set().AsQueryable().ToList(); - - foreach (var fs in allFollowedStreams) - _streamTracker.AddLastData(fs.CreateKey(), null, false); - - _trackCounter = allFollowedStreams.GroupBy(x => new - { - x.Type, - Name = x.Username.ToLower() - }) - .ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name), - x => x.Select(fs => fs.GuildId).ToHashSet()); - } - } - - _pubSub.Sub(StreamsOfflineKey, HandleStreamsOffline); - _pubSub.Sub(StreamsOnlineKey, HandleStreamsOnline); - - if (client.ShardId == 0) - { - // only shard 0 will run the tracker, - // and then publish updates with redis to other shards - _streamTracker.OnStreamsOffline += OnStreamsOffline; - _streamTracker.OnStreamsOnline += OnStreamsOnline; - _ = _streamTracker.RunAsync(); - - _pubSub.Sub(_streamFollowKey, HandleFollowStream); - _pubSub.Sub(_streamUnfollowKey, HandleUnfollowStream); - } - - bot.JoinedGuild += ClientOnJoinedGuild; - client.LeftGuild += ClientOnLeftGuild; - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(30)); - while (await timer.WaitForNextTickAsync()) - { - try - { - var errorLimit = TimeSpan.FromHours(12); - var failingStreams = _streamTracker.GetFailingStreams(errorLimit, true).ToList(); - - if (!failingStreams.Any()) - continue; - - var deleteGroups = failingStreams.GroupBy(x => x.Type) - .ToDictionary(x => x.Key, x => x.Select(y => y.Name).ToList()); - - await using var uow = _db.GetDbContext(); - foreach (var kvp in deleteGroups) - { - Log.Information( - "Deleting {StreamCount} {Platform} streams because they've been erroring for more than {ErrorLimit}: {RemovedList}", - kvp.Value.Count, - kvp.Key, - errorLimit, - string.Join(", ", kvp.Value)); - - var toDelete = uow.Set() - .AsQueryable() - .Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username)) - .ToList(); - - uow.RemoveRange(toDelete); - await uow.SaveChangesAsync(); - - foreach (var loginToDelete in kvp.Value) - _streamTracker.UntrackStreamByKey(new(kvp.Key, loginToDelete)); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error cleaning up FollowedStreams"); - } - } - } - - /// - /// Handles follow stream pubs to keep the counter up to date. - /// When counter reaches 0, stream is removed from tracking because - /// that means no guilds are subscribed to that stream anymore - /// - private ValueTask HandleFollowStream(FollowStreamPubData info) - { - _streamTracker.AddLastData(info.Key, null, false); - lock (_shardLock) - { - var key = info.Key; - if (_trackCounter.ContainsKey(key)) - _trackCounter[key].Add(info.GuildId); - else - { - _trackCounter[key] = new() - { - info.GuildId - }; - } - } - - return default; - } - - /// - /// Handles unfollow pubs to keep the counter up to date. - /// When counter reaches 0, stream is removed from tracking because - /// that means no guilds are subscribed to that stream anymore - /// - private ValueTask HandleUnfollowStream(FollowStreamPubData info) - { - lock (_shardLock) - { - var key = info.Key; - if (!_trackCounter.TryGetValue(key, out var set)) - { - // it should've been removed already? - _streamTracker.UntrackStreamByKey(in key); - return default; - } - - set.Remove(info.GuildId); - if (set.Count != 0) - return default; - - _trackCounter.Remove(key); - // if no other guilds are following this stream - // untrack the stream - _streamTracker.UntrackStreamByKey(in key); - } - - return default; - } - - private async ValueTask HandleStreamsOffline(List offlineStreams) - { - foreach (var stream in offlineStreams) - { - var key = stream.CreateKey(); - if (_shardTrackedStreams.TryGetValue(key, out var fss)) - { - await fss - // send offline stream notifications only to guilds which enable it with .stoff - .SelectMany(x => x.Value) - .Where(x => _offlineNotificationServers.Contains(x.GuildId)) - .Select(fs => _client.GetGuild(fs.GuildId) - ?.GetTextChannel(fs.ChannelId) - ?.EmbedAsync(GetEmbed(fs.GuildId, stream))) - .WhenAll(); - } - } - } - - - private async ValueTask HandleStreamsOnline(List onlineStreams) - { - foreach (var stream in onlineStreams) - { - var key = stream.CreateKey(); - if (_shardTrackedStreams.TryGetValue(key, out var fss)) - { - var messages = await fss.SelectMany(x => x.Value) - .Select(async fs => - { - var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId); - - if (textChannel is null) - return default; - - var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username) - .WithOverride("%platform%", () => fs.Type.ToString()) - .Build(); - - var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message); - - var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream, false), message); - - // only cache the ids of channel/message pairs - if(_deleteOnOfflineServers.Contains(fs.GuildId)) - return (textChannel.Id, msg.Id); - else - return default; - }) - .WhenAll(); - - - // push online stream messages to redis - // when streams go offline, any server which - // has the online stream message deletion feature - // enabled will have the online messages deleted - try - { - var pairs = messages - .Where(x => x != default) - .Select(x => (x.Item1, x.Item2)) - .ToList(); - - if (pairs.Count > 0) - await OnlineMessagesSent(key.Type, key.Name, pairs); - } - catch - { - - } - } - } - } - - private Task OnStreamsOnline(List data) - => _pubSub.Pub(StreamsOnlineKey, data); - - private Task OnStreamsOffline(List data) - => _pubSub.Pub(StreamsOfflineKey, data); - - private Task ClientOnJoinedGuild(GuildConfig guildConfig) - { - using (var uow = _db.GetDbContext()) - { - var gc = uow.Set().AsQueryable() - .Include(x => x.FollowedStreams) - .FirstOrDefault(x => x.GuildId == guildConfig.GuildId); - - if (gc is null) - return Task.CompletedTask; - - if (gc.NotifyStreamOffline) - _offlineNotificationServers.Add(gc.GuildId); - - foreach (var followedStream in gc.FollowedStreams) - { - var key = followedStream.CreateKey(); - var streams = GetLocalGuildStreams(key, gc.GuildId); - streams.Add(followedStream); - PublishFollowStream(followedStream); - } - } - - return Task.CompletedTask; - } - - private Task ClientOnLeftGuild(SocketGuild guild) - { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.FollowedStreams)); - - _offlineNotificationServers.TryRemove(gc.GuildId); - - foreach (var followedStream in gc.FollowedStreams) - { - var streams = GetLocalGuildStreams(followedStream.CreateKey(), guild.Id); - streams.Remove(followedStream); - - PublishUnfollowStream(followedStream); - } - } - - return Task.CompletedTask; - } - - public async Task ClearAllStreams(ulong guildId) - { - await using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams)); - uow.RemoveRange(gc.FollowedStreams); - - foreach (var s in gc.FollowedStreams) - await PublishUnfollowStream(s); - - uow.SaveChanges(); - - return gc.FollowedStreams.Count; - } - - public async Task UnfollowStreamAsync(ulong guildId, int index) - { - FollowedStream fs; - await using (var uow = _db.GetDbContext()) - { - var fss = uow.Set() - .AsQueryable() - .Where(x => x.GuildId == guildId) - .OrderBy(x => x.Id) - .ToList(); - - // out of range - if (fss.Count <= index) - return null; - - fs = fss[index]; - uow.Remove(fs); - - await uow.SaveChangesAsync(); - - // remove from local cache - lock (_shardLock) - { - var key = fs.CreateKey(); - var streams = GetLocalGuildStreams(key, guildId); - streams.Remove(fs); - } - } - - await PublishUnfollowStream(fs); - - return fs; - } - - private void PublishFollowStream(FollowedStream fs) - => _pubSub.Pub(_streamFollowKey, - new() - { - Key = fs.CreateKey(), - GuildId = fs.GuildId - }); - - private Task PublishUnfollowStream(FollowedStream fs) - => _pubSub.Pub(_streamUnfollowKey, - new() - { - Key = fs.CreateKey(), - GuildId = fs.GuildId - }); - - public async Task FollowStream(ulong guildId, ulong channelId, string url) - { - // this will - var data = await _streamTracker.GetStreamDataByUrlAsync(url); - - if (data is null) - return null; - - FollowedStream fs; - await using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams)); - - // add it to the database - fs = new() - { - Type = data.StreamType, - Username = data.UniqueName, - ChannelId = channelId, - GuildId = guildId - }; - - if (gc.FollowedStreams.Count >= 10) - return null; - - gc.FollowedStreams.Add(fs); - await uow.SaveChangesAsync(); - - // add it to the local cache of tracked streams - // this way this shard will know it needs to post a message to discord - // when shard 0 publishes stream status changes for this stream - lock (_shardLock) - { - var key = data.CreateKey(); - var streams = GetLocalGuildStreams(key, guildId); - streams.Add(fs); - } - } - - PublishFollowStream(fs); - - return data; - } - - public IEmbedBuilder GetEmbed(ulong guildId, StreamData status, bool showViewers = true) - { - var embed = _eb.Create() - .WithTitle(status.Name) - .WithUrl(status.StreamUrl) - .WithDescription(status.StreamUrl) - .AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true); - - if (showViewers) - { - embed.AddField(GetText(guildId, strs.viewers), - status.Viewers == 0 && !status.IsLive - ? "-" - : status.Viewers, - true); - } - - if (status.IsLive) - embed = embed.WithOkColor(); - else - embed = embed.WithErrorColor(); - - if (!string.IsNullOrWhiteSpace(status.Title)) - embed.WithAuthor(status.Title); - - if (!string.IsNullOrWhiteSpace(status.Game)) - embed.AddField(GetText(guildId, strs.streaming), status.Game, true); - - if (!string.IsNullOrWhiteSpace(status.AvatarUrl)) - embed.WithThumbnailUrl(status.AvatarUrl); - - if (!string.IsNullOrWhiteSpace(status.Preview)) - embed.WithImageUrl(status.Preview + "?dv=" + _rng.Next()); - - return embed; - } - - private string GetText(ulong guildId, LocStr str) - => _strings.GetText(str, guildId); - - public bool ToggleStreamOffline(ulong guildId) - { - bool newValue; - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - newValue = gc.NotifyStreamOffline = !gc.NotifyStreamOffline; - uow.SaveChanges(); - - if (newValue) - _offlineNotificationServers.Add(guildId); - else - _offlineNotificationServers.TryRemove(guildId); - - return newValue; - } - - public bool ToggleStreamOnlineDelete(ulong guildId) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - var newValue = gc.DeleteStreamOnlineMessage = !gc.DeleteStreamOnlineMessage; - uow.SaveChanges(); - - if (newValue) - _deleteOnOfflineServers.Add(guildId); - else - _deleteOnOfflineServers.TryRemove(guildId); - - return newValue; - } - - public Task GetStreamDataAsync(string url) - => _streamTracker.GetStreamDataByUrlAsync(url); - - private HashSet GetLocalGuildStreams(in StreamDataKey key, ulong guildId) - { - if (_shardTrackedStreams.TryGetValue(key, out var map)) - { - if (map.TryGetValue(guildId, out var set)) - return set; - return map[guildId] = new(); - } - - _shardTrackedStreams[key] = new() - { - { guildId, new() } - }; - return _shardTrackedStreams[key][guildId]; - } - - public bool SetStreamMessage( - ulong guildId, - int index, - string message, - out FollowedStream fs) - { - using var uow = _db.GetDbContext(); - var fss = uow.Set().AsQueryable().Where(x => x.GuildId == guildId).OrderBy(x => x.Id).ToList(); - - if (fss.Count <= index) - { - fs = null; - return false; - } - - fs = fss[index]; - fs.Message = message; - lock (_shardLock) - { - var streams = GetLocalGuildStreams(fs.CreateKey(), guildId); - - // message doesn't participate in equality checking - // removing and adding = update - streams.Remove(fs); - streams.Add(fs); - } - - uow.SaveChanges(); - - return true; - } - - public int SetStreamMessageForAll(ulong guildId, string message) - { - using var uow = _db.GetDbContext(); - - var all = uow.Set().ToList(); - - if (all.Count == 0) - return 0; - - all.ForEach(x => x.Message = message); - - uow.SaveChanges(); - - return all.Count; - } - - public sealed class FollowStreamPubData - { - public StreamDataKey Key { get; init; } - public ulong GuildId { get; init; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamOnlineMessageDeleterService.cs b/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamOnlineMessageDeleterService.cs deleted file mode 100644 index 549ac8f..0000000 --- a/src/Ellie.Bot.Modules.Searches/StreamNotification/StreamOnlineMessageDeleterService.cs +++ /dev/null @@ -1,99 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Db.Models; -using Ellie.Modules.Searches.Common; - -namespace Ellie.Modules.Searches.Services; - -public sealed class StreamOnlineMessageDeleterService : IEService, IReadyExecutor -{ - private readonly StreamNotificationService _notifService; - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly IPubSub _pubSub; - - public StreamOnlineMessageDeleterService( - StreamNotificationService notifService, - DbService db, - IPubSub pubSub, - DiscordSocketClient client) - { - _notifService = notifService; - _db = db; - _client = client; - _pubSub = pubSub; - } - - public async Task OnReadyAsync() - { - _notifService.OnlineMessagesSent += OnOnlineMessagesSent; - - if (_client.ShardId == 0) - await _pubSub.Sub(_notifService.StreamsOfflineKey, OnStreamsOffline); - } - - private async Task OnOnlineMessagesSent( - FollowedStream.FType type, - string name, - IReadOnlyCollection<(ulong, ulong)> pairs) - { - await using var ctx = _db.GetDbContext(); - foreach (var (channelId, messageId) in pairs) - { - await ctx.GetTable() - .InsertAsync(() => new() - { - Name = name, - Type = type, - MessageId = messageId, - ChannelId = channelId, - DateAdded = DateTime.UtcNow, - }); - } - } - - private async ValueTask OnStreamsOffline(List streamDatas) - { - if (_client.ShardId != 0) - return; - - var pairs = await GetMessagesToDelete(streamDatas); - - foreach (var (channelId, messageId) in pairs) - { - try - { - var textChannel = await _client.GetChannelAsync(channelId) as ITextChannel; - if (textChannel is null) - continue; - - await textChannel.DeleteMessageAsync(messageId); - } - catch - { - continue; - } - } - } - - private async Task> GetMessagesToDelete(List streamDatas) - { - await using var ctx = _db.GetDbContext(); - - var toReturn = new List<(ulong, ulong)>(); - foreach (var sd in streamDatas) - { - var key = sd.CreateKey(); - var toDelete = await ctx.GetTable() - .Where(x => (x.Type == key.Type && x.Name == key.Name) - || Sql.DateDiff(Sql.DateParts.Day, x.DateAdded, DateTime.UtcNow) > 1) - .DeleteWithOutputAsync(); - - toReturn.AddRange(toDelete.Select(x => (x.ChannelId, x.MessageId))); - } - - return toReturn; - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Translate/ITranslateService.cs b/src/Ellie.Bot.Modules.Searches/Translate/ITranslateService.cs deleted file mode 100644 index c42de45..0000000 --- a/src/Ellie.Bot.Modules.Searches/Translate/ITranslateService.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Searches; - -public interface ITranslateService -{ - public Task Translate(string source, string target, string text = null); - Task ToggleAtl(ulong guildId, ulong channelId, bool autoDelete); - IEnumerable GetLanguages(); - - Task RegisterUserAsync( - ulong userId, - ulong channelId, - string from, - string to); - - Task UnregisterUser(ulong channelId, ulong userId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Translate/TranslateService.cs b/src/Ellie.Bot.Modules.Searches/Translate/TranslateService.cs deleted file mode 100644 index 822b28b..0000000 --- a/src/Ellie.Bot.Modules.Searches/Translate/TranslateService.cs +++ /dev/null @@ -1,224 +0,0 @@ -#nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Common.ModuleBehaviors; -using Ellie.Services.Database.Models; -using System.Net; - -namespace Ellie.Modules.Searches; - -public sealed class TranslateService : ITranslateService, IExecNoCommand, IReadyExecutor, IEService -{ - private readonly IGoogleApiService _google; - private readonly DbService _db; - private readonly IEmbedBuilderService _eb; - private readonly IBot _bot; - - private readonly ConcurrentDictionary _atcs = new(); - private readonly ConcurrentDictionary> _users = new(); - - public TranslateService( - IGoogleApiService google, - DbService db, - IEmbedBuilderService eb, - IBot bot) - { - _google = google; - _db = db; - _eb = eb; - _bot = bot; - } - - public async Task OnReadyAsync() - { - List cs; - await using (var ctx = _db.GetDbContext()) - { - var guilds = _bot.AllGuildConfigs.Select(x => x.GuildId).ToList(); - cs = await ctx.Set().Include(x => x.Users) - .Where(x => guilds.Contains(x.GuildId)) - .ToListAsyncEF(); - } - - foreach (var c in cs) - { - _atcs[c.ChannelId] = c.AutoDelete; - _users[c.ChannelId] = - new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower()))); - } - } - - - public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) - { - if (string.IsNullOrWhiteSpace(msg.Content)) - return; - - if (msg is { Channel: ITextChannel tch } um) - { - if (!_atcs.TryGetValue(tch.Id, out var autoDelete)) - return; - - if (!_users.TryGetValue(tch.Id, out var users) || !users.TryGetValue(um.Author.Id, out var langs)) - return; - - var output = await _google.Translate(msg.Content, langs.From, langs.To); - - if (string.IsNullOrWhiteSpace(output) - || msg.Content.Equals(output, StringComparison.InvariantCultureIgnoreCase)) - return; - - var embed = _eb.Create().WithOkColor(); - - if (autoDelete) - { - embed.WithAuthor(um.Author.ToString(), um.Author.GetAvatarUrl()) - .AddField(langs.From, um.Content) - .AddField(langs.To, output); - - await tch.EmbedAsync(embed); - - try - { - await um.DeleteAsync(); - } - catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) - { - _atcs.TryUpdate(tch.Id, false, true); - } - - return; - } - - await um.ReplyAsync(embed: embed.AddField(langs.To, output).Build(), allowedMentions: AllowedMentions.None); - } - } - - public async Task Translate(string source, string target, string text = null) - { - if (string.IsNullOrWhiteSpace(text)) - throw new ArgumentException("Text is empty or null", nameof(text)); - - var res = await _google.Translate(text, source, target); - return res.SanitizeMentions(true); - } - - public async Task ToggleAtl(ulong guildId, ulong channelId, bool autoDelete) - { - await using var ctx = _db.GetDbContext(); - - var old = await ctx.Set().ToLinqToDBTable() - .FirstOrDefaultAsyncLinqToDB(x => x.ChannelId == channelId); - - if (old is null) - { - ctx.Set().Add(new() - { - GuildId = guildId, - ChannelId = channelId, - AutoDelete = autoDelete - }); - - await ctx.SaveChangesAsync(); - - _atcs[channelId] = autoDelete; - _users[channelId] = new(); - - return true; - } - - // if autodelete value is different, update the autodelete value - // instead of disabling - if (old.AutoDelete != autoDelete) - { - old.AutoDelete = autoDelete; - await ctx.SaveChangesAsync(); - _atcs[channelId] = autoDelete; - return true; - } - - await ctx.Set().ToLinqToDBTable().DeleteAsync(x => x.ChannelId == channelId); - - await ctx.SaveChangesAsync(); - _atcs.TryRemove(channelId, out _); - _users.TryRemove(channelId, out _); - - return false; - } - - - private void UpdateUser( - ulong channelId, - ulong userId, - string from, - string to) - { - var dict = _users.GetOrAdd(channelId, new ConcurrentDictionary()); - dict[userId] = (from, to); - } - - public async Task RegisterUserAsync( - ulong userId, - ulong channelId, - string from, - string to) - { - if (!_google.Languages.ContainsKey(from) || !_google.Languages.ContainsKey(to)) - return null; - - await using var ctx = _db.GetDbContext(); - var ch = await ctx.Set().GetByChannelId(channelId); - - if (ch is null) - return null; - - var user = ch.Users.FirstOrDefault(x => x.UserId == userId); - - if (user is null) - { - ch.Users.Add(user = new() - { - Source = from, - Target = to, - UserId = userId - }); - - await ctx.SaveChangesAsync(); - - UpdateUser(channelId, userId, from, to); - - return true; - } - - // if it's different from old settings, update - if (user.Source != from || user.Target != to) - { - user.Source = from; - user.Target = to; - - await ctx.SaveChangesAsync(); - - UpdateUser(channelId, userId, from, to); - - return true; - } - - return await UnregisterUser(channelId, userId); - } - - public async Task UnregisterUser(ulong channelId, ulong userId) - { - await using var ctx = _db.GetDbContext(); - var rows = await ctx.Set().ToLinqToDBTable() - .DeleteAsync(x => x.UserId == userId && x.Channel.ChannelId == channelId); - - if (_users.TryGetValue(channelId, out var inner)) - inner.TryRemove(userId, out _); - - return rows > 0; - } - - public IEnumerable GetLanguages() - => _google.Languages.GroupBy(x => x.Value).Select(x => $"{x.AsEnumerable().Select(y => y.Key).Join(", ")}"); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/Translate/TranslatorCommands.cs b/src/Ellie.Bot.Modules.Searches/Translate/TranslatorCommands.cs deleted file mode 100644 index 95afb61..0000000 --- a/src/Ellie.Bot.Modules.Searches/Translate/TranslatorCommands.cs +++ /dev/null @@ -1,96 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class TranslateCommands : EllieModule - { - public enum AutoDeleteAutoTranslate - { - Del, - Nodel - } - - [Cmd] - public async Task Translate(string from, string to, [Leftover] string text = null) - { - try - { - await ctx.Channel.TriggerTypingAsync(); - var translation = await _service.Translate(from, to, text); - - var embed = _eb.Create(ctx).WithOkColor().AddField(from, text).AddField(to, translation); - - await ctx.Channel.EmbedAsync(embed); - } - catch - { - await ReplyErrorLocalizedAsync(strs.bad_input_format); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [OwnerOnly] - public async Task AutoTranslate(AutoDeleteAutoTranslate autoDelete = AutoDeleteAutoTranslate.Nodel) - { - var toggle = - await _service.ToggleAtl(ctx.Guild.Id, ctx.Channel.Id, autoDelete == AutoDeleteAutoTranslate.Del); - if (toggle) - await ReplyConfirmLocalizedAsync(strs.atl_started); - else - { - await ReplyConfirmLocalizedAsync(strs.atl_stopped); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AutoTransLang() - { - if (await _service.UnregisterUser(ctx.Channel.Id, ctx.User.Id)) - await ReplyConfirmLocalizedAsync(strs.atl_removed); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task AutoTransLang(string from, string to) - { - var succ = await _service.RegisterUserAsync(ctx.User.Id, ctx.Channel.Id, from.ToLower(), to.ToLower()); - - if (succ is null) - { - await ReplyErrorLocalizedAsync(strs.atl_not_enabled); - return; - } - - if (succ is false) - { - await ReplyErrorLocalizedAsync(strs.invalid_lang); - return; - } - - await ReplyConfirmLocalizedAsync(strs.atl_set(from, to)); - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Translangs() - { - var langs = _service.GetLanguages().ToList(); - - var eb = _eb.Create() - .WithTitle(GetText(strs.supported_languages)) - .WithOkColor(); - - foreach (var chunk in langs.Chunk(15)) - { - eb.AddField("󠀁", chunk.Join("\n"), isInline: true); - } - - await ctx.Channel.EmbedAsync(eb); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/XkcdCommands.cs b/src/Ellie.Bot.Modules.Searches/XkcdCommands.cs deleted file mode 100644 index d33f478..0000000 --- a/src/Ellie.Bot.Modules.Searches/XkcdCommands.cs +++ /dev/null @@ -1,97 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class XkcdCommands : EllieModule - { - private const string XKCD_URL = "https://xkcd.com"; - private readonly IHttpClientFactory _httpFactory; - - public XkcdCommands(IHttpClientFactory factory) - => _httpFactory = factory; - - [Cmd] - [Priority(0)] - public async Task Xkcd(string arg = null) - { - if (arg?.ToLowerInvariant().Trim() == "latest") - { - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync($"{XKCD_URL}/info.0.json"); - var comic = JsonConvert.DeserializeObject(res); - var embed = _eb.Create() - .WithOkColor() - .WithImageUrl(comic.ImageLink) - .WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{XKCD_URL}/{comic.Num}") - .AddField(GetText(strs.comic_number), comic.Num.ToString(), true) - .AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true); - var sent = await ctx.Channel.EmbedAsync(embed); - - await Task.Delay(10000); - - await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt).Build()); - } - catch (HttpRequestException) - { - await ReplyErrorLocalizedAsync(strs.comic_not_found); - } - - return; - } - - await Xkcd(new EllieRandom().Next(1, 1750)); - } - - [Cmd] - [Priority(1)] - public async Task Xkcd(int num) - { - if (num < 1) - return; - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync($"{XKCD_URL}/{num}/info.0.json"); - - var comic = JsonConvert.DeserializeObject(res); - var embed = _eb.Create() - .WithOkColor() - .WithImageUrl(comic.ImageLink) - .WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{XKCD_URL}/{num}") - .AddField(GetText(strs.comic_number), comic.Num.ToString(), true) - .AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true); - - var sent = await ctx.Channel.EmbedAsync(embed); - - await Task.Delay(10000); - - await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt).Build()); - } - catch (HttpRequestException) - { - await ReplyErrorLocalizedAsync(strs.comic_not_found); - } - } - } - - public class XkcdComic - { - public int Num { get; set; } - public string Month { get; set; } - public string Year { get; set; } - - [JsonProperty("safe_title")] - public string Title { get; set; } - - [JsonProperty("img")] - public string ImageLink { get; set; } - - public string Alt { get; set; } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtTrackService.cs b/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtTrackService.cs deleted file mode 100644 index 291ab78..0000000 --- a/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtTrackService.cs +++ /dev/null @@ -1,134 +0,0 @@ -#nullable disable - -// public class YtTrackService : IEService -// { -// private readonly IGoogleApiService _google; -// private readonly IHttpClientFactory httpClientFactory; -// private readonly DiscordSocketClient _client; -// private readonly DbService _db; -// private readonly ConcurrentDictionary>> followedChannels; -// private readonly ConcurrentDictionary _latestPublishes = new ConcurrentDictionary(); -// -// public YtTrackService(IGoogleApiService google, IHttpClientFactory httpClientFactory, DiscordSocketClient client, -// DbService db) -// { -// this._google = google; -// this.httpClientFactory = httpClientFactory; -// this._client = client; -// this._db = db; -// -// if (_client.ShardId == 0) -// { -// _ = CheckLoop(); -// } -// } -// -// public async Task CheckLoop() -// { -// while (true) -// { -// await Task.Delay(10000); -// using (var http = httpClientFactory.CreateClient()) -// { -// await followedChannels.Select(kvp => CheckChannel(kvp.Key, kvp.Value.SelectMany(x => x.Value).ToList())).WhenAll(); -// } -// } -// } -// -// /// -// /// Checks the specified youtube channel, and sends a message to all provided -// /// -// /// Id of the youtube channel -// /// Where to post updates if there is a new update -// private async Task CheckChannel(string youtubeChannelId, List followedChannels) -// { -// var latestVid = (await _google.GetLatestChannelVideosAsync(youtubeChannelId, 1)) -// .FirstOrDefault(); -// if (latestVid is null) -// { -// return; -// } -// -// if (_latestPublishes.TryGetValue(youtubeChannelId, out var latestPub) && latestPub >= latestVid.PublishedAt) -// { -// return; -// } -// _latestPublishes[youtubeChannelId] = latestVid.PublishedAt; -// -// foreach (var chObj in followedChannels) -// { -// var gCh = _client.GetChannel(chObj.ChannelId); -// if (gCh is ITextChannel ch) -// { -// var msg = latestVid.GetVideoUrl(); -// if (!string.IsNullOrWhiteSpace(chObj.UploadMessage)) -// msg = chObj.UploadMessage + Environment.NewLine + msg; -// -// await ch.SendMessageAsync(msg); -// } -// } -// } -// -// /// -// /// Starts posting updates on the specified discord channel when a new video is posted on the specified YouTube channel. -// /// -// /// Id of the discord guild -// /// Id of the discord channel -// /// Id of the youtube channel -// /// Message to post when a new video is uploaded, along with video URL -// /// Whether adding was successful -// public async Task ToggleChannelFollowAsync(ulong guildId, ulong channelId, string ytChannelId, string uploadMessage) -// { -// // to to see if we can get a video from that channel -// var vids = await _google.GetLatestChannelVideosAsync(ytChannelId, 1); -// if (vids.Count == 0) -// return false; -// -// using(var uow = _db.GetDbContext()) -// { -// var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.YtFollowedChannels)); -// -// // see if this yt channel was already followed on this discord channel -// var oldObj = gc.YtFollowedChannels -// .FirstOrDefault(x => x.ChannelId == channelId && x.YtChannelId == ytChannelId); -// -// if(oldObj is not null) -// { -// return false; -// } -// -// // can only add up to 10 tracked channels per server -// if (gc.YtFollowedChannels.Count >= 10) -// { -// return false; -// } -// -// var obj = new YtFollowedChannel -// { -// ChannelId = channelId, -// YtChannelId = ytChannelId, -// UploadMessage = uploadMessage -// }; -// -// // add to database -// gc.YtFollowedChannels.Add(obj); -// -// // add to the local cache: -// -// // get follows on all guilds -// var allGuildFollows = followedChannels.GetOrAdd(ytChannelId, new ConcurrentDictionary>()); -// // add to this guild's follows -// allGuildFollows.AddOrUpdate(guildId, -// new List(), -// (key, old) => -// { -// old.Add(obj); -// return old; -// }); -// -// await uow.SaveChangesAsync(); -// } -// -// return true; -// } -// } \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtUploadCommands.cs b/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtUploadCommands.cs deleted file mode 100644 index 3d2d678..0000000 --- a/src/Ellie.Bot.Modules.Searches/YoutubeTrack/YtUploadCommands.cs +++ /dev/null @@ -1,54 +0,0 @@ -#nullable disable -namespace Ellie.Modules.Searches; - -public partial class Searches -{ - // [Group] - // public partial class YtTrackCommands : EllieModule - // { - // ; - // [RequireContext(ContextType.Guild)] - // public async Task YtFollow(string ytChannelId, [Leftover] string uploadMessage = null) - // { - // var succ = await _service.ToggleChannelFollowAsync(ctx.Guild.Id, ctx.Channel.Id, ytChannelId, uploadMessage); - // if(succ) - // { - // await ReplyConfirmLocalizedAsync(strs.yt_follow_added); - // } - // else - // { - // await ReplyConfirmLocalizedAsync(strs.yt_follow_fail); - // } - // } - // - // [EllieCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // public async Task YtTrackRm(int index) - // { - // //var succ = await _service.ToggleChannelTrackingAsync(ctx.Guild.Id, ctx.Channel.Id, ytChannelId, uploadMessage); - // //if (succ) - // //{ - // // await ReplyConfirmLocalizedAsync(strs.yt_track_added); - // //} - // //else - // //{ - // // await ReplyConfirmLocalizedAsync(strs.yt_track_fail); - // //} - // } - // - // [EllieCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // public async Task YtTrackList() - // { - // //var succ = await _service.ToggleChannelTrackingAsync(ctx.Guild.Id, ctx.Channel.Id, ytChannelId, uploadMessage); - // //if (succ) - // //{ - // // await ReplyConfirmLocalizedAsync(strs.yt_track_added); - // //} - // //else - // //{ - // // await ReplyConfirmLocalizedAsync(strs.yt_track_fail); - // //} - // } - // } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/AltExtensions.cs b/src/Ellie.Bot.Modules.Searches/_common/AltExtensions.cs deleted file mode 100644 index 82fdbd7..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/AltExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable disable -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Ellie.Services.Database.Models; - -namespace Ellie.Modules.Searches; - -public static class AltExtensions -{ - public static Task GetByChannelId(this IQueryable set, ulong channelId) - => set.Include(x => x.Users).FirstOrDefaultAsyncEF(x => x.ChannelId == channelId); -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/BibleVerses.cs b/src/Ellie.Bot.Modules.Searches/_common/BibleVerses.cs deleted file mode 100644 index c280b7d..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/BibleVerses.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Modules.Searches.Common; - -// todo replace newtonsoft with json.text -public class BibleVerses -{ - public string Error { get; set; } - public BibleVerse[] Verses { get; set; } -} - -public class BibleVerse -{ - [JsonProperty("book_name")] - public string BookName { get; set; } - - public int Chapter { get; set; } - public int Verse { get; set; } - public string Text { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/Config/ImgSearchEngine.cs b/src/Ellie.Bot.Modules.Searches/_common/Config/ImgSearchEngine.cs deleted file mode 100644 index 6132c28..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/Config/ImgSearchEngine.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ellie.Modules.Searches; - -public enum ImgSearchEngine -{ - Google, - Searx, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfig.cs b/src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfig.cs deleted file mode 100644 index d7f38d9..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfig.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Cloneable; -using Ellie.Common.Yml; - -namespace Ellie.Modules.Searches; - -[Cloneable] -public partial class SearchesConfig : ICloneable -{ - [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 0; - - [Comment(""" - Which engine should .search command - 'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys. - 'google' - official google api. 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_Scrape; - - [Comment(""" - Which engine should .image command use - 'google'- official google api. 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.Ytdlp; - - [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/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfigService.cs b/src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfigService.cs deleted file mode 100644 index 34edc0c..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/Config/SearchesConfigService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Ellie.Common.Configs; - -namespace Ellie.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); - - Migrate(); - } - - private void Migrate() - { - if (data.Version < 1) - { - ModifyConfig(c => - { - c.Version = 1; - c.WebSearchEngine = WebSearchEngine.Google_Scrape; - }); - } - } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/Config/WebSearchEngine.cs b/src/Ellie.Bot.Modules.Searches/_common/Config/WebSearchEngine.cs deleted file mode 100644 index 7d16a86..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/Config/WebSearchEngine.cs +++ /dev/null @@ -1,9 +0,0 @@ -// ReSharper disable InconsistentNaming -namespace Ellie.Modules.Searches; - -public enum WebSearchEngine -{ - Google, - Google_Scrape, - Searx, -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/CryptoData.cs b/src/Ellie.Bot.Modules.Searches/_common/CryptoData.cs deleted file mode 100644 index cd21545..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/CryptoData.cs +++ /dev/null @@ -1,66 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace Ellie.Modules.Searches.Common; - -public class CryptoResponse -{ - public List Data { get; set; } -} - -public class CmcQuote -{ - [JsonPropertyName("price")] - public double Price { get; set; } - - [JsonPropertyName("volume_24h")] - public double Volume24h { get; set; } - - // [JsonPropertyName("volume_change_24h")] - // public double VolumeChange24h { get; set; } - // - // [JsonPropertyName("percent_change_1h")] - // public double PercentChange1h { get; set; } - - [JsonPropertyName("percent_change_24h")] - public double PercentChange24h { get; set; } - - [JsonPropertyName("percent_change_7d")] - public double PercentChange7d { get; set; } - - [JsonPropertyName("market_cap")] - public double MarketCap { get; set; } - - [JsonPropertyName("market_cap_dominance")] - public double MarketCapDominance { get; set; } -} - -public class CmcResponseData -{ - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("symbol")] - public string Symbol { get; set; } - - [JsonPropertyName("slug")] - public string Slug { get; set; } - - [JsonPropertyName("cmc_rank")] - public int CmcRank { get; set; } - - [JsonPropertyName("circulating_supply")] - public double? CirculatingSupply { get; set; } - - [JsonPropertyName("total_supply")] - public double? TotalSupply { get; set; } - - [JsonPropertyName("max_supply")] - public double? MaxSupply { get; set; } - - [JsonPropertyName("quote")] - public Dictionary Quote { get; set; } -} \ No newline at end of file diff --git a/src/Ellie.Bot.Modules.Searches/_common/DefineModel.cs b/src/Ellie.Bot.Modules.Searches/_common/DefineModel.cs deleted file mode 100644 index 4bee77c..0000000 --- a/src/Ellie.Bot.Modules.Searches/_common/DefineModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace Ellie.Modules.Searches.Common; - -public class Audio -{ - public string Url { get; set; } -} - -public class Example -{ - public List