From 01194beb9d3986d5d5026893075f658a20ab2cc8 Mon Sep 17 00:00:00 2001 From: Ge Date: Mon, 29 Apr 2024 23:54:27 +0800 Subject: [PATCH] Implement nullable feature (#10) * Update doc * Working head * working head * working head * working head * working head * Working head * Working head * Workflow build on PR * Fix badge * Final commit * clean up * Fixes * Fix sample and test null warning --- .editorconfig | 41 +- .github/workflows/pr.yml | 16 + .../workflows/{push-or-pr.yml => push.yml} | 4 +- Kook.Net.Sample.targets | 19 + Kook.Net.Test.targets | 37 + Kook.Net.sln | 9 + Kook.Net.targets | 4 +- README.md | 2 +- THIRD-PARTY-NOTICES.txt | 52 +- docs/_template/material/public/main.js | 3 + .../Kook.Net.Samples.Audio.csproj | 19 +- .../Modules/MusicModule.cs | 26 +- samples/Kook.Net.Samples.Audio/Program.cs | 7 +- .../Services/CommandHandlingService.cs | 10 +- .../Services/KookClientService.cs | 4 +- .../Services/MusicService.cs | 30 +- .../Extensions/TemplateExtensions.cs | 46 +- .../Kook.Net.Samples.CardMarkup.csproj | 17 +- .../Modules/CardCommandModule.cs | 11 +- .../Kook.Net.Samples.CardMarkup/Program.cs | 6 +- .../Services/CommandHandlerService.cs | 21 +- .../Services/KookSocketService.cs | 5 +- .../Kook.Net.Samples.Docker.csproj | 24 +- samples/Kook.Net.Samples.Docker/Program.cs | 5 +- .../Kook.Net.Samples.FSharp.fsproj | 16 +- .../Configurations/KookBotConfigurations.cs | 6 +- .../Extensions/KookBotClientExtension.cs | 8 +- .../KookBotClientExtensionEventHandlers.cs | 28 +- .../Kook.Net.Samples.ReactionRoleBot.csproj | 19 +- .../Kook.Net.Samples.SimpleBot.csproj | 15 +- samples/Kook.Net.Samples.SimpleBot/Program.cs | 4 +- .../Kook.Net.Samples.TextCommands.csproj | 17 +- .../Modules/PublicModule.cs | 57 +- .../Kook.Net.Samples.TextCommands/Program.cs | 60 +- .../Services/CommandHandlingService.cs | 15 +- .../Services/KookClientService.cs | 53 + .../Services/PictureService.cs | 10 +- .../Kook.Net.Samples.VisualBasic.vbproj | 11 +- .../CardMarkupSerializer.cs | 83 +- .../Extensions/CardBuilderExtensions.cs | 61 +- .../Extensions/DictionaryExtensions.cs | 11 +- .../Extensions/ParserExtensions.cs | 5 +- .../Models/MarkupElement.cs | 8 +- .../Attributes/AliasAttribute.cs | 7 +- .../Attributes/CommandAttribute.cs | 21 +- .../Attributes/DontAutoLoadAttribute.cs | 6 +- .../Attributes/DontInjectAttribute.cs | 6 +- .../Attributes/GroupAttribute.cs | 14 +- .../Attributes/NameAttribute.cs | 7 +- .../Attributes/NamedArgumentTypeAttribute.cs | 6 +- .../Attributes/OverrideTypeReaderAttribute.cs | 3 +- .../ParameterPreconditionAttribute.cs | 6 +- .../Attributes/PreconditionAttribute.cs | 6 +- .../RequireBotPermissionAttribute.cs | 27 +- .../Preconditions/RequireContextAttribute.cs | 25 +- .../Preconditions/RequireRoleAttribute.cs | 23 +- .../Preconditions/RequireUserAttribute.cs | 9 +- .../RequireUserPermissionAttribute.cs | 11 +- .../Attributes/PriorityAttribute.cs | 7 +- .../Attributes/RemainderAttribute.cs | 6 +- .../Attributes/RemarksAttribute.cs | 7 +- .../Attributes/SummaryAttribute.cs | 7 +- .../Builders/CommandBuilder.cs | 41 +- .../Builders/ModuleBuilder.cs | 54 +- .../Builders/ModuleClassBuilder.cs | 132 +- .../Builders/ParameterBuilder.cs | 44 +- src/Kook.Net.Commands/CommandContext.cs | 2 +- src/Kook.Net.Commands/CommandException.cs | 2 +- src/Kook.Net.Commands/CommandMatch.cs | 20 +- src/Kook.Net.Commands/CommandParser.cs | 58 +- src/Kook.Net.Commands/CommandService.cs | 173 +- src/Kook.Net.Commands/CommandServiceConfig.cs | 4 +- src/Kook.Net.Commands/EmptyServiceProvider.cs | 2 +- .../Extensions/CommandServiceExtensions.cs | 28 +- .../Extensions/MessageExtensions.cs | 31 +- src/Kook.Net.Commands/IModuleBase.cs | 2 +- src/Kook.Net.Commands/Info/CommandInfo.cs | 151 +- src/Kook.Net.Commands/Info/ModuleInfo.cs | 59 +- src/Kook.Net.Commands/Info/ParameterInfo.cs | 22 +- src/Kook.Net.Commands/Map/CommandMap.cs | 11 +- src/Kook.Net.Commands/Map/CommandMapNode.cs | 75 +- src/Kook.Net.Commands/ModuleBase.cs | 75 +- .../Readers/ChannelTypeReader.cs | 8 +- .../Readers/EnumTypeReader.cs | 24 +- .../Readers/MessageTypeReader.cs | 7 +- .../Readers/NamedArgumentTypeReader.cs | 97 +- .../Readers/NullableTypeReader.cs | 13 +- .../Readers/PrimitiveTypeReader.cs | 10 +- .../Readers/RoleTypeReader.cs | 11 +- .../Readers/TimeSpanTypeReader.cs | 16 +- .../Readers/UserTypeReader.cs | 81 +- .../Results/ExecuteResult.cs | 20 +- src/Kook.Net.Commands/Results/IResult.cs | 2 +- src/Kook.Net.Commands/Results/MatchResult.cs | 24 +- src/Kook.Net.Commands/Results/ParseResult.cs | 52 +- .../Results/PreconditionGroupResult.cs | 23 +- .../Results/PreconditionResult.cs | 18 +- .../Results/RuntimeResult.cs | 4 +- src/Kook.Net.Commands/Results/SearchResult.cs | 23 +- .../Results/TypeReaderResult.cs | 46 +- .../Utilities/QuotationAliasUtils.cs | 2 +- .../Utilities/ReflectionUtils.cs | 44 +- src/Kook.Net.Core/AssemblyInfo.cs | 1 + src/Kook.Net.Core/Audio/AudioStream.cs | 2 +- src/Kook.Net.Core/Audio/IAudioClient.cs | 2 - src/Kook.Net.Core/Commands/ICommandContext.cs | 2 +- .../Entities/Activities/GameProperties.cs | 4 +- .../Entities/Activities/IActivity.cs | 4 +- .../Entities/Activities/IGame.cs | 6 +- .../Entities/Activities/Music.cs | 4 +- .../CreateCategoryChannelProperties.cs | 4 +- .../Channels/CreateGuildChannelProperties.cs | 4 +- .../Entities/Channels/IAudioChannel.cs | 7 +- .../Entities/Channels/ICategoryChannel.cs | 4 +- .../Entities/Channels/IChannel.cs | 4 +- .../Entities/Channels/IDMChannel.cs | 22 +- .../Entities/Channels/IGuildChannel.cs | 20 +- .../Entities/Channels/IMessageChannel.cs | 46 +- .../Entities/Channels/INestedChannel.cs | 13 +- .../Entities/Channels/ITextChannel.cs | 4 +- .../Entities/Channels/IVoiceChannel.cs | 8 +- .../Channels/ModifyGuildChannelProperties.cs | 2 +- .../Channels/ModifyTextChannelProperties.cs | 4 +- .../Channels/ModifyVoiceChannelProperties.cs | 4 +- src/Kook.Net.Core/Entities/Emotes/Emoji.cs | 43 +- src/Kook.Net.Core/Entities/Emotes/Emote.cs | 63 +- .../Entities/Emotes/GuildEmote.cs | 4 +- .../Entities/Guilds/GuildFeatures.cs | 32 +- src/Kook.Net.Core/Entities/Guilds/IGuild.cs | 93 +- .../Entities/Guilds/IRecommendInfo.cs | 2 +- src/Kook.Net.Core/Entities/Guilds/RoleType.cs | 2 +- src/Kook.Net.Core/Entities/IDeletable.cs | 2 +- src/Kook.Net.Core/Entities/IUpdateable.cs | 2 +- src/Kook.Net.Core/Entities/Image.cs | 8 +- .../Entities/Intimacies/IIntimacy.cs | 4 +- .../Entities/Intimacies/IntimacyProperties.cs | 14 +- src/Kook.Net.Core/Entities/Invites/IInvite.cs | 18 +- .../Entities/Messages/Cards/Card.cs | 26 +- .../Entities/Messages/Cards/CardBuilder.cs | 61 +- .../Elements/Builders/ButtonElementBuilder.cs | 127 +- .../Elements/Builders/ImageElementBuilder.cs | 96 +- .../Builders/KMarkdownElementBuilder.cs | 89 +- .../Builders/ParagraphStructBuilder.cs | 188 +- .../Builders/PlainTextElementBuilder.cs | 87 +- .../Messages/Cards/Elements/ButtonElement.cs | 32 +- .../Messages/Cards/Elements/ImageElement.cs | 30 +- .../Cards/Elements/KMarkdownElement.cs | 31 +- .../Cards/Elements/ParagraphStruct.cs | 32 +- .../Cards/Elements/PlainTextElement.cs | 30 +- .../Cards/Modules/ActionGroupModule.cs | 31 +- .../Messages/Cards/Modules/AudioModule.cs | 32 +- .../Builders/ActionGroupModuleBuilder.cs | 105 +- .../Modules/Builders/AudioModuleBuilder.cs | 73 +- .../Builders/ContainerModuleBuilder.cs | 112 +- .../Modules/Builders/ContextModuleBuilder.cs | 150 +- .../Builders/CountdownModuleBuilder.cs | 37 +- .../Modules/Builders/DividerModuleBuilder.cs | 26 +- .../Modules/Builders/FileModuleBuilder.cs | 50 +- .../Modules/Builders/HeaderModuleBuilder.cs | 103 +- .../Builders/ImageGroupModuleBuilder.cs | 89 +- .../Modules/Builders/InviteModuleBuilder.cs | 48 +- .../Modules/Builders/SectionModuleBuilder.cs | 100 +- .../Modules/Builders/VideoModuleBuilder.cs | 50 +- .../Messages/Cards/Modules/ContainerModule.cs | 31 +- .../Messages/Cards/Modules/ContextModule.cs | 31 +- .../Messages/Cards/Modules/CountdownModule.cs | 26 +- .../Messages/Cards/Modules/DividerModule.cs | 26 +- .../Messages/Cards/Modules/FileModule.cs | 30 +- .../Messages/Cards/Modules/HeaderModule.cs | 33 +- .../Messages/Cards/Modules/IMediaModule.cs | 2 +- .../Cards/Modules/ImageGroupModule.cs | 31 +- .../Messages/Cards/Modules/InviteModule.cs | 33 +- .../Cards/Modules/SectionAccessoryMode.cs | 7 +- .../Messages/Cards/Modules/SectionModule.cs | 35 +- .../Messages/Cards/Modules/VideoModule.cs | 30 +- .../Messages/Embeds/BilibiliVideoEmbed.cs | 9 +- .../Entities/Messages/Embeds/CardEmbed.cs | 20 + .../Entities/Messages/Embeds/EmbedType.cs | 7 +- .../Entities/Messages/Embeds/IEmbed.cs | 8 - .../Entities/Messages/Embeds/ImageEmbed.cs | 9 +- .../Entities/Messages/Embeds/LinkEmbed.cs | 7 +- .../Messages/Embeds/NotImplementedEmbed.cs | 11 +- .../Entities/Messages/FileAttachment.cs | 24 +- .../Entities/Messages/IAttachment.cs | 4 +- .../Entities/Messages/IMessage.cs | 12 +- src/Kook.Net.Core/Entities/Messages/IQuote.cs | 29 +- src/Kook.Net.Core/Entities/Messages/ITag.cs | 2 +- .../Entities/Messages/IUserMessage.cs | 4 +- .../Entities/Messages/MessageProperties.cs | 8 +- .../Entities/Messages/MessageReference.cs | 35 + .../ImageAnimationPokeResource.cs | 7 +- .../NotImplementedPokeResource.cs | 7 +- src/Kook.Net.Core/Entities/Messages/Quote.cs | 52 +- src/Kook.Net.Core/Entities/Messages/Tag.cs | 8 +- .../Entities/Messages/TagHandling.cs | 28 +- .../Permissions/ChannelPermissions.cs | 14 +- .../Entities/Permissions/GuildPermission.cs | 2 +- .../Entities/Permissions/GuildPermissions.cs | 29 +- .../Permissions/OverwritePermissions.cs | 22 +- .../Entities/Roles/AlphaColor.cs | 53 +- src/Kook.Net.Core/Entities/Roles/Color.cs | 45 +- .../Entities/Roles/GradientColor.cs | 4 +- src/Kook.Net.Core/Entities/Roles/IRole.cs | 8 +- .../Entities/Roles/RoleProperties.cs | 4 +- .../Users/BoostSubscriptionMetadata.cs | 2 +- .../Entities/Users/ClientType.cs | 1 + .../Entities/Users/IFriendRequest.cs | 4 +- .../Entities/Users/IGuildUser.cs | 44 +- src/Kook.Net.Core/Entities/Users/ISelfUser.cs | 10 +- src/Kook.Net.Core/Entities/Users/IUser.cs | 22 +- .../Entities/Users/IVoiceState.cs | 2 +- src/Kook.Net.Core/Entities/Users/Nameplate.cs | 25 +- .../Users/SearchGuildMemberProperties.cs | 2 +- src/Kook.Net.Core/Entities/Users/UserTag.cs | 26 +- .../Extensions/CardExtensions.cs | 169 +- .../Extensions/CollectionExtensions.cs | 40 +- .../Extensions/MessageExtensions.cs | 85 +- .../TaskCompletionSourceExtensions.cs | 24 +- .../Extensions/UserExtensions.cs | 58 +- src/Kook.Net.Core/Format.cs | 65 +- src/Kook.Net.Core/IKookClient.cs | 22 +- src/Kook.Net.Core/Kook.Net.Core.csproj | 9 +- src/Kook.Net.Core/KookConfig.cs | 4 +- src/Kook.Net.Core/Logging/LogManager.cs | 67 +- src/Kook.Net.Core/Logging/LogMessage.cs | 62 +- src/Kook.Net.Core/Logging/Logger.cs | 67 +- src/Kook.Net.Core/Net/BucketId.cs | 51 +- src/Kook.Net.Core/Net/HttpException.cs | 31 +- src/Kook.Net.Core/Net/RateLimitedException.cs | 4 +- src/Kook.Net.Core/Net/Rest/IRateLimitInfo.cs | 4 +- src/Kook.Net.Core/Net/Rest/IRestClient.cs | 16 +- src/Kook.Net.Core/Net/Rest/RestResponse.cs | 7 +- src/Kook.Net.Core/Net/Udp/IUdpSocket.cs | 2 +- .../Net/WebSocketClosedException.cs | 10 +- .../Net/WebSockets/IWebSocketClient.cs | 6 +- src/Kook.Net.Core/RequestOptions.cs | 24 +- src/Kook.Net.Core/Utils/AsyncEvent.cs | 34 +- src/Kook.Net.Core/Utils/Cacheable.cs | 41 +- src/Kook.Net.Core/Utils/ConcurrentHashSet.cs | 125 +- src/Kook.Net.Core/Utils/MentionUtils.cs | 234 +- .../Utils/Paging/PagedEnumerator.cs | 14 +- src/Kook.Net.Core/Utils/Permissions.cs | 92 +- src/Kook.Net.Core/Utils/Preconditions.cs | 229 +- src/Kook.Net.Core/Utils/RoleUtils.cs | 5 +- src/Kook.Net.Core/Utils/TokenUtils.cs | 16 +- src/Kook.Net.Core/Utils/UrlValidation.cs | 12 +- src/Kook.Net.Core/Utils/ValueHelper.cs | 14 + .../Core/Entities/Guilds/GuildProperties.cs | 15 +- .../Rest/API/Common/VoiceRegion.cs | 4 +- .../Rest/API/Rest/CreateGuildParams.cs | 4 +- .../Rest/API/Rest/DeleteGuildParams.cs | 2 +- .../Rest/API/Rest/DisconnectUserParams.cs | 4 +- .../Rest/API/Rest/ModifyGuildParams.cs | 2 +- .../Rest/API/Rest/ValidateCardsParams.cs | 2 +- .../Channels/ExperimentalChannelHelper.cs | 17 +- .../RestTextChannelExperimentalExtensions.cs | 4 +- .../RestVoiceChannelExperimentalExtensions.cs | 8 +- .../Guilds/ExperimentalGuildHelper.cs | 23 +- .../Guilds/RestGuildExperimentalExtensions.cs | 6 +- .../Rest/Entities/Guilds/RestVoiceRegion.cs | 1 + .../Rest/ExperimentalClientHelper.cs | 34 +- ...KookRestApiClientExperimentalExtensions.cs | 50 +- .../KookRestClientExperimentalExtensions.cs | 34 +- .../Converters/ImageBase64DataUriConverter.cs | 7 +- .../BaseSocketClientExperimentalExtensions.cs | 6 +- ...ocketVoiceChannelExperimentalExtensions.cs | 4 +- .../SocketGuildExperimentalExtensions.cs | 8 +- src/Kook.Net.Rest/API/Common/Attachment.cs | 8 +- src/Kook.Net.Rest/API/Common/Ban.cs | 8 +- src/Kook.Net.Rest/API/Common/Cards/Card.cs | 6 +- .../API/Common/Cards/CardBase.cs | 2 +- .../Common/Cards/Elements/ButtonElement.cs | 8 +- .../API/Common/Cards/Elements/ElementBase.cs | 2 +- .../API/Common/Cards/Elements/ImageElement.cs | 4 +- .../Common/Cards/Elements/KMarkdownElement.cs | 2 +- .../Common/Cards/Elements/ParagraphStruct.cs | 4 +- .../Common/Cards/Elements/PlainTextElement.cs | 4 +- .../Common/Cards/Modules/ActionGroupModule.cs | 2 +- .../API/Common/Cards/Modules/AudioModule.cs | 6 +- .../Common/Cards/Modules/ContainerModule.cs | 2 +- .../API/Common/Cards/Modules/ContextModule.cs | 3 +- .../API/Common/Cards/Modules/FileModule.cs | 4 +- .../API/Common/Cards/Modules/HeaderModule.cs | 2 +- .../Common/Cards/Modules/ImageGroupModule.cs | 2 +- .../API/Common/Cards/Modules/InviteModule.cs | 2 +- .../API/Common/Cards/Modules/ModuleBase.cs | 2 +- .../API/Common/Cards/Modules/SectionModule.cs | 8 +- .../API/Common/Cards/Modules/VideoModule.cs | 4 +- src/Kook.Net.Rest/API/Common/Channel.cs | 16 +- src/Kook.Net.Rest/API/Common/DirectMessage.cs | 24 +- .../API/Common/Embeds/BilibiliVideoEmbed.cs | 15 +- .../API/Common/Embeds/CardEmbed.cs | 23 + .../API/Common/Embeds/EmbedBase.cs | 3 - src/Kook.Net.Rest/API/Common/Embeds/IEmbed.cs | 2 - .../API/Common/Embeds/ImageEmbed.cs | 5 +- .../API/Common/Embeds/LinkEmbed.cs | 13 +- .../API/Common/Embeds/NotImplementedEmbed.cs | 3 +- src/Kook.Net.Rest/API/Common/Emoji.cs | 12 +- src/Kook.Net.Rest/API/Common/Game.cs | 14 +- src/Kook.Net.Rest/API/Common/Guild.cs | 12 +- .../API/Common/GuildMentionInfo.cs | 9 + src/Kook.Net.Rest/API/Common/Intimacy.cs | 12 +- src/Kook.Net.Rest/API/Common/Invite.cs | 22 +- src/Kook.Net.Rest/API/Common/MentionInfo.cs | 9 +- .../API/Common/MentionedChannel.cs | 2 +- src/Kook.Net.Rest/API/Common/MentionedUser.cs | 6 +- src/Kook.Net.Rest/API/Common/Message.cs | 30 +- src/Kook.Net.Rest/API/Common/Nameplate.cs | 14 +- .../Pokes/ImageAnimationPokeResource.cs | 16 +- src/Kook.Net.Rest/API/Common/Pokes/Poke.cs | 26 +- .../API/Common/Pokes/PokeQualityResource.cs | 6 +- .../API/Common/Pokes/PokeResourceBase.cs | 2 +- src/Kook.Net.Rest/API/Common/Quote.cs | 13 +- src/Kook.Net.Rest/API/Common/Reaction.cs | 2 +- src/Kook.Net.Rest/API/Common/RecommendInfo.cs | 18 +- src/Kook.Net.Rest/API/Common/Role.cs | 4 +- src/Kook.Net.Rest/API/Common/User.cs | 16 +- src/Kook.Net.Rest/API/Common/UserChat.cs | 11 +- .../API/Common/UserPermissionOverwrite.cs | 2 +- src/Kook.Net.Rest/API/Common/UserTag.cs | 2 +- src/Kook.Net.Rest/API/Net/MultipartFile.cs | 6 +- .../API/Rest/AddOrRemoveRoleParams.cs | 6 +- .../API/Rest/AddOrRemoveRoleResponsecs.cs | 2 +- .../API/Rest/AddReactionParams.cs | 4 +- .../API/Rest/BeginActivityParams.cs | 9 +- src/Kook.Net.Rest/API/Rest/BlockUserParams.cs | 2 +- .../API/Rest/BoostSubscription.cs | 2 +- .../API/Rest/CreateAssetParams.cs | 21 +- .../API/Rest/CreateAssetResponse.cs | 2 +- .../API/Rest/CreateDirectMessageParams.cs | 20 +- .../API/Rest/CreateDirectMessageResponse.cs | 2 +- .../API/Rest/CreateGameParams.cs | 6 +- .../API/Rest/CreateGuildBanParams.cs | 6 +- .../API/Rest/CreateGuildChannelParams.cs | 8 +- .../API/Rest/CreateGuildEmoteParams.cs | 30 +- .../API/Rest/CreateGuildInviteResponse.cs | 2 +- .../API/Rest/CreateGuildRoleParams.cs | 4 +- .../API/Rest/CreateMessageParams.cs | 15 +- .../API/Rest/CreateMessageResponse.cs | 2 +- ...odifyChannelPermissionOverwriteResponse.cs | 17 +- ...rRemoveChannelPermissionOverwriteParams.cs | 13 +- .../Rest/CreateOrRemoveGuildMuteDeafParams.cs | 6 +- .../API/Rest/CreateUserChatParams.cs | 2 +- .../API/Rest/DeleteDirectMessageParams.cs | 2 +- .../API/Rest/DeleteGameParams.cs | 2 +- .../API/Rest/DeleteGuildChannelParams.cs | 2 +- .../API/Rest/DeleteGuildEmoteParams.cs | 2 +- .../API/Rest/DeleteGuildInviteParams.cs | 2 +- .../API/Rest/DeleteGuildRoleParams.cs | 4 +- .../API/Rest/DeleteMessageParams.cs | 2 +- .../API/Rest/DeleteUserChatParams.cs | 2 +- .../API/Rest/EndGameActivityParams.cs | 5 +- src/Kook.Net.Rest/API/Rest/ExtendedGuild.cs | 6 +- src/Kook.Net.Rest/API/Rest/FriendState.cs | 2 +- .../API/Rest/GetBotGatewayResponse.cs | 2 +- .../GetChannelPermissionOverwritesResponse.cs | 4 +- .../API/Rest/GetFriendStatesResponse.cs | 6 +- .../API/Rest/GetGuildMuteDeafListResponse.cs | 6 +- .../API/Rest/GetVoiceGatewayResponse.cs | 2 +- src/Kook.Net.Rest/API/Rest/GuildMember.cs | 24 +- .../API/Rest/HandleFriendRequestParams.cs | 4 +- .../API/Rest/KickOutGuildMemberParams.cs | 4 +- .../API/Rest/LeaveGuildParams.cs | 2 +- .../ModifyChannelPermissionOverwriteParams.cs | 14 +- .../API/Rest/ModifyDirectMessageParams.cs | 10 +- .../API/Rest/ModifyGameParams.cs | 6 +- .../API/Rest/ModifyGuildChannelParams.cs | 4 +- .../API/Rest/ModifyGuildEmoteParams.cs | 4 +- .../Rest/ModifyGuildMemberNicknameParams.cs | 6 +- .../API/Rest/ModifyGuildRoleParams.cs | 6 +- .../API/Rest/ModifyMessageParams.cs | 10 +- .../API/Rest/ModifyTextChannelParams.cs | 2 +- .../API/Rest/ModifyVoiceChannelParams.cs | 4 +- src/Kook.Net.Rest/API/Rest/MoveUsersParams.cs | 4 +- .../API/Rest/PagedResponseBase.cs | 8 +- .../API/Rest/QueryMessagesResponse.cs | 2 +- .../API/Rest/QueryUserChatMessagesResponse.cs | 2 +- .../API/Rest/RemoveFriendParams.cs | 2 +- .../API/Rest/RemoveGuildBanParams.cs | 4 +- .../API/Rest/RemoveReactionParams.cs | 4 +- .../API/Rest/RequestFriendParams.cs | 8 +- .../API/Rest/RestResponseBase.cs | 4 +- src/Kook.Net.Rest/API/Rest/RichGuild.cs | 8 +- src/Kook.Net.Rest/API/Rest/SelfUser.cs | 6 +- .../API/Rest/SyncChannelPermissionsParams.cs | 4 +- .../API/Rest/UnblockUserParams.cs | 2 +- .../API/Rest/UpdateIntimacyValueParams.cs | 6 +- src/Kook.Net.Rest/BaseKookClient.cs | 80 +- src/Kook.Net.Rest/ClientHelper.cs | 137 +- .../Entities/Channels/ChannelHelper.cs | 567 ++--- .../Entities/Channels/IRestAudioChannel.cs | 4 +- .../Entities/Channels/IRestMessageChannel.cs | 9 +- .../Entities/Channels/RestCategoryChannel.cs | 10 +- .../Entities/Channels/RestChannel.cs | 18 +- .../Entities/Channels/RestDMChannel.cs | 183 +- .../Entities/Channels/RestGuildChannel.cs | 205 +- .../Entities/Channels/RestTextChannel.cs | 252 +-- .../Entities/Channels/RestVoiceChannel.cs | 79 +- .../Entities/Games/GameHelper.cs | 20 +- src/Kook.Net.Rest/Entities/Games/RestGame.cs | 23 +- .../Entities/Guilds/GuildHelper.cs | 279 ++- .../Entities/Guilds/RecommendInfo.cs | 18 +- src/Kook.Net.Rest/Entities/Guilds/RestBan.cs | 5 +- .../Entities/Guilds/RestGuild.cs | 533 +++-- .../Entities/Intimacies/IntimacyHelper.cs | 10 +- .../Entities/Intimacies/RestIntimacy.cs | 12 +- .../Entities/Invites/InviteHelper.cs | 10 +- .../Entities/Invites/RestInvite.cs | 74 +- .../Entities/Messages/Attachment.cs | 14 +- .../Entities/Messages/MessageHelper.cs | 550 +++-- src/Kook.Net.Rest/Entities/Messages/Poke.cs | 10 +- .../Entities/Messages/RestMessage.cs | 123 +- .../Entities/Messages/RestReaction.cs | 9 +- .../Entities/Messages/RestSystemMessage.cs | 4 +- .../Entities/Messages/RestUserMessage.cs | 204 +- src/Kook.Net.Rest/Entities/Roles/RestRole.cs | 53 +- .../Entities/Roles/RoleHelper.cs | 16 +- .../Entities/Users/RestFriendRequest.cs | 31 +- .../Entities/Users/RestGuildUser.cs | 127 +- .../Entities/Users/RestPresence.cs | 27 +- .../Entities/Users/RestSelfUser.cs | 27 +- src/Kook.Net.Rest/Entities/Users/RestUser.cs | 90 +- .../Entities/Users/UserHelper.cs | 159 +- .../Extensions/CardJsonExtension.cs | 51 +- .../Extensions/EntityExtensions.cs | 506 ++--- src/Kook.Net.Rest/Kook.Net.Rest.csproj | 4 +- src/Kook.Net.Rest/KookRestApiClient.cs | 907 ++++---- src/Kook.Net.Rest/KookRestClient.cs | 160 +- .../Cards/ButtonClickEventTypeConverter.cs | 2 +- .../Converters/Cards/ButtonThemeConverter.cs | 2 +- .../Net/Converters/Cards/CardConverter.cs | 8 +- .../Converters/Cards/CardConverterFactory.cs | 28 + .../Net/Converters/Cards/CardSizeConverter.cs | 2 +- .../Converters/Cards/CardThemeConverter.cs | 2 +- .../Net/Converters/Cards/CardTypeConverter.cs | 2 +- .../Cards/CountdownModeConverter.cs | 2 +- .../Net/Converters/Cards/ElementConverter.cs | 8 +- .../Converters/Cards/ElementTypeConverter.cs | 2 +- .../Cards/HexAlphaColorConverter.cs | 9 +- .../Net/Converters/Cards/HexColorConverter.cs | 4 +- .../Converters/Cards/ImageSizeConverter.cs | 2 +- .../Net/Converters/Cards/ModuleConverter.cs | 8 +- .../Converters/Cards/ModuleTypeConverter.cs | 2 +- .../Cards/RawValueColorConverter.cs | 3 +- .../Cards/SectionAccessoryModeConvertercs.cs | 12 +- .../Net/Converters/ChatCodeConverter.cs | 5 +- .../Net/Converters/Embeds/EmbedConverter.cs | 14 +- .../Converters/Embeds/EmbedTypeConverter.cs | 4 +- .../Net/Converters/FriendStateConverter.cs | 2 +- .../Net/Converters/GuildFeaturesConverter.cs | 6 +- .../Net/Converters/MusicProviderConverter.cs | 2 +- .../Converters/NullableChatCodeConverter.cs | 11 +- .../NullableDateTimeOffsetConverter.cs | 23 - ...TimeOffsetUnixTimeMillisecondsConverter.cs | 33 + .../NullableGradientColorConverter.cs | 8 +- .../Net/Converters/NullableGuidConverter.cs | 11 +- .../Converters/NullableTimeSpanConverter.cs | 8 +- .../Net/Converters/NullableUInt32Converter.cs | 7 +- .../Net/Converters/NullableUInt64Converter.cs | 7 +- .../NullableVoiceQualityConverter.cs | 5 +- .../Net/Converters/PageSortInfoConverter.cs | 9 +- .../Converters/Pokes/PokeResourceConverter.cs | 10 +- .../Pokes/PokeResourceTypeConverter.cs | 2 +- .../Net/Converters/QuoteConverter.cs | 2 +- .../Net/Converters/SafeAttachmentConverter.cs | 96 + .../Net/Converters/SafeUInt64Converter.cs | 25 + src/Kook.Net.Rest/Net/DefaultRestClient.cs | 56 +- src/Kook.Net.Rest/Net/Queue/ClientBucket.cs | 27 +- src/Kook.Net.Rest/Net/Queue/GatewayBucket.cs | 28 +- src/Kook.Net.Rest/Net/Queue/RequestQueue.cs | 46 +- .../Net/Queue/RequestQueueBucket.cs | 137 +- .../Net/Queue/Requests/JsonRestRequest.cs | 9 +- .../Queue/Requests/MultipartRestRequest.cs | 12 +- .../Net/Queue/Requests/RestRequest.cs | 5 +- .../Net/Queue/Requests/WebSocketRequest.cs | 14 +- src/Kook.Net.Rest/Net/RateLimitInfo.cs | 14 +- .../Gateway/DirectMessageButtonClickEvent.cs | 27 + .../API/Gateway/DirectMessageUpdateEvent.cs | 13 +- .../API/Gateway/GatewayEvent.cs | 18 +- .../Gateway/GatewayGroupMessageExtraData.cs | 24 +- .../API/Gateway/GatewayHelloPayload.cs | 1 - .../Gateway/GatewayPersonMessageExtraData.cs | 17 +- .../API/Gateway/GatewayReconnectPayload.cs | 2 +- .../API/Gateway/GatewaySocketFrame.cs | 3 +- .../API/Gateway/GatewaySocketFrameType.cs | 14 +- .../Gateway/GatewaySystemEventExtraData.cs | 5 +- .../API/Gateway/GuildBanEvent.cs | 4 +- .../API/Gateway/GuildEmojiEvent.cs | 4 +- .../API/Gateway/GuildEvent.cs | 15 +- .../API/Gateway/GuildMemberOnlineEvent.cs | 17 - ...nt.cs => GuildMemberOnlineOfflineEvent.cs} | 6 +- .../API/Gateway/GuildMemberUpdateEvent.cs | 2 +- .../API/Gateway/KMarkdownInfo.cs | 8 +- .../API/Gateway/MessageButtonClickEvent.cs | 8 +- ...nnedMessageEvent.cs => MessagePinEvent.cs} | 2 +- .../API/Gateway/MessageUpdateEvent.cs | 21 +- .../API/Gateway/PrivateReaction.cs | 2 +- .../API/Gateway/Reaction.cs | 2 +- .../API/Gateway/UnpinnedMessageEvent.cs | 15 - .../API/Gateway/UserUpdateEvent.cs | 4 +- .../API/Voice/CreatePlainTransportParams.cs | 6 +- .../API/Voice/CreatePlainTransportResponse.cs | 2 +- .../API/Voice/JoinParams.cs | 2 +- .../API/Voice/ProduceParams.cs | 28 +- .../API/Voice/VoiceSocketIncomeFrame.cs | 2 +- .../API/Voice/VoiceSocketRequestFrame.cs | 8 +- src/Kook.Net.WebSocket/Audio/AudioClient.cs | 163 +- .../Audio/Opus/OpusConverter.cs | 6 +- .../Audio/Opus/OpusEncoder.cs | 23 +- .../Audio/Streams/BufferedWriteStream.cs | 14 +- .../Audio/Streams/OpusEncodeStream.cs | 8 +- .../Audio/Streams/RtpWriteStream.cs | 10 +- .../BaseSocketClient.Event.cs | 46 +- src/Kook.Net.WebSocket/BaseSocketClient.cs | 60 +- src/Kook.Net.WebSocket/ClientState.cs | 89 +- .../Commands/SocketCommandContext.cs | 4 +- src/Kook.Net.WebSocket/ConnectionManager.cs | 82 +- .../Entities/Channels/ISocketAudioChannel.cs | 4 +- .../Channels/ISocketMessageChannel.cs | 2 +- .../Channels/SocketCategoryChannel.cs | 58 +- .../Entities/Channels/SocketChannel.cs | 20 +- .../Entities/Channels/SocketChannelHelper.cs | 85 +- .../Entities/Channels/SocketDMChannel.cs | 211 +- .../Entities/Channels/SocketGuildChannel.cs | 194 +- .../Entities/Channels/SocketTextChannel.cs | 255 ++- .../Entities/Channels/SocketVoiceChannel.cs | 85 +- .../Entities/Guilds/SocketGuild.cs | 651 +++--- .../Entities/Guilds/SocketGuildHelper.cs | 11 +- .../Entities/Invites/SocketInvite.cs | 59 +- .../Entities/Messages/MessageCache.cs | 63 +- .../Entities/Messages/SocketMessage.cs | 207 +- .../Entities/Messages/SocketMessageHelper.cs | 51 +- .../Entities/Messages/SocketPokeAction.cs | 5 +- .../Entities/Messages/SocketReaction.cs | 57 +- .../Entities/Messages/SocketSystemMessage.cs | 33 +- .../Entities/Messages/SocketUserMessage.cs | 399 ++-- .../Entities/Roles/SocketRole.cs | 54 +- .../Entities/Users/SocketGlobalUser.cs | 34 +- .../Entities/Users/SocketGuildUser.cs | 169 +- .../Entities/Users/SocketPresence.cs | 39 +- .../Entities/Users/SocketSelfUser.cs | 92 +- .../Entities/Users/SocketUnknownUser.cs | 27 +- .../Entities/Users/SocketUser.cs | 173 +- .../Entities/Users/SocketUserHelper.cs | 12 +- .../Entities/Users/SocketVoiceState.cs | 33 +- .../Extensions/SocketEntityExtensions.cs | 4 +- .../Kook.Net.WebSocket.csproj | 8 +- src/Kook.Net.WebSocket/KookSocketApiClient.cs | 158 +- .../KookSocketClient.Messages.cs | 1251 +++++++++++ src/Kook.Net.WebSocket/KookSocketClient.cs | 1996 ++++------------- src/Kook.Net.WebSocket/KookSocketConfig.cs | 5 +- .../KookSocketRestClient.cs | 16 +- src/Kook.Net.WebSocket/KookVoiceAPIClient.cs | 198 +- .../GatewaySocketFrameTypeConverter.cs | 14 - .../Net/Converters/MessageTypeConverter.cs | 11 - .../VoiceSocketFrameTypeConverter.cs | 4 - .../Net/DefaultUdpSocket.cs | 19 +- .../Net/DefaultUdpSocketProvider.cs | 3 +- .../Net/DefaultWebSocketClient.cs | 120 +- .../Net/DefaultWebSocketClientProvider.cs | 2 +- .../ChannelTests.cs | 11 +- .../Fixtures/KookRestClientFixture.cs | 14 +- .../Fixtures/RestChannelFixture.cs | 2 +- .../Fixtures/RestGuildFixture.cs | 4 +- test/Kook.Net.Tests.Integration/GuildTests.cs | 16 +- .../Kook.Net.Tests.Integration.csproj | 33 +- .../KookRestApiClientTests.cs | 22 +- .../MessageInTextTests.cs | 24 +- .../MessageInVoiceTests.cs | 24 +- test/Kook.Net.Tests.Integration/RoleTests.cs | 2 +- test/Kook.Net.Tests.Unit/CardBuilderTests.cs | 17 +- test/Kook.Net.Tests.Unit/CardXmlTests.cs | 16 +- .../ChannelPermissionsTests.cs | 2 - .../ElementBuilderTests.cs | 95 +- test/Kook.Net.Tests.Unit/EmojiTests.cs | 4 +- test/Kook.Net.Tests.Unit/FormatTests.cs | 18 +- .../Kook.Net.Tests.Unit.csproj | 34 +- test/Kook.Net.Tests.Unit/MentionUtilsTests.cs | 24 +- .../Kook.Net.Tests.Unit/MessageHelperTests.cs | 16 +- .../MockedEntities/MockedCategoryChannel.cs | 26 +- .../MockedEntities/MockedDMChannel.cs | 126 +- .../MockedEntities/MockedInvalidChannel.cs | 6 +- .../MockedEntities/MockedTextChannel.cs | 121 +- .../MockedEntities/MockedVoiceChannel.cs | 128 +- .../Kook.Net.Tests.Unit/ModuleBuilderTests.cs | 158 +- .../TimeSpanTypeReaderTests.cs | 21 +- test/Kook.Net.Tests.Unit/TokenUtilsTests.cs | 11 +- test/Kook.Net.Tests.Unit/TypeReaderTests.cs | 32 +- .../Kook.Net.Tests.Unit/UrlValidationTests.cs | 21 +- 589 files changed, 12233 insertions(+), 11377 deletions(-) create mode 100644 .github/workflows/pr.yml rename .github/workflows/{push-or-pr.yml => push.yml} (86%) create mode 100644 Kook.Net.Sample.targets create mode 100644 Kook.Net.Test.targets create mode 100644 samples/Kook.Net.Samples.TextCommands/Services/KookClientService.cs create mode 100644 src/Kook.Net.Core/Entities/Messages/Embeds/CardEmbed.cs create mode 100644 src/Kook.Net.Core/Entities/Messages/MessageReference.cs create mode 100644 src/Kook.Net.Core/Utils/ValueHelper.cs create mode 100644 src/Kook.Net.Rest/API/Common/Embeds/CardEmbed.cs create mode 100644 src/Kook.Net.Rest/API/Common/GuildMentionInfo.cs create mode 100644 src/Kook.Net.Rest/Net/Converters/Cards/CardConverterFactory.cs delete mode 100644 src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetConverter.cs create mode 100644 src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetUnixTimeMillisecondsConverter.cs create mode 100644 src/Kook.Net.Rest/Net/Converters/SafeAttachmentConverter.cs create mode 100644 src/Kook.Net.Rest/Net/Converters/SafeUInt64Converter.cs create mode 100644 src/Kook.Net.WebSocket/API/Gateway/DirectMessageButtonClickEvent.cs delete mode 100644 src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineEvent.cs rename src/Kook.Net.WebSocket/API/Gateway/{GuildMemberOfflineEvent.cs => GuildMemberOnlineOfflineEvent.cs} (67%) rename src/Kook.Net.WebSocket/API/Gateway/{PinnedMessageEvent.cs => MessagePinEvent.cs} (90%) delete mode 100644 src/Kook.Net.WebSocket/API/Gateway/UnpinnedMessageEvent.cs create mode 100644 src/Kook.Net.WebSocket/KookSocketClient.Messages.cs delete mode 100644 src/Kook.Net.WebSocket/Net/Converters/GatewaySocketFrameTypeConverter.cs delete mode 100644 src/Kook.Net.WebSocket/Net/Converters/MessageTypeConverter.cs diff --git a/.editorconfig b/.editorconfig index 3b18bc30..148124a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ +root = true # editorconfig.org # top-most EditorConfig file -root = true # Default settings: # A newline ending every file @@ -19,23 +19,24 @@ generated_code = true # C# files [*.cs] # New line preferences -csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true +csharp_indent_case_contents_when_block = false csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current # Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion # avoid this. unless absolutely necessary @@ -44,15 +45,15 @@ dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion -# Types: use keywords instead of BCL types, and permit var only when the type is clear +# Types: use keywords instead of BCL types, and use explicit type instead of var csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_when_type_is_apparent = false:suggestion csharp_style_var_elsewhere = false:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion # name all constant fields using PascalCase -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = silent +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field @@ -81,7 +82,7 @@ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case # Code style defaults csharp_using_directive_placement = outside_namespace:suggestion dotnet_sort_system_directives_first = true -csharp_prefer_braces = when_multiline:silent +csharp_prefer_braces = when_multiline:suggestion csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = false csharp_prefer_static_local_function = true:suggestion @@ -151,8 +152,30 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# ReSharper properties +resharper_braces_for_for = required_for_multiline +resharper_braces_for_while = required_for_multiline +resharper_constructor_or_destructor_body = block_body +resharper_indent_nested_fixed_stmt = true +resharper_indent_nested_foreach_stmt = true +resharper_indent_nested_for_stmt = true +resharper_indent_nested_lock_stmt = true +resharper_indent_nested_usings_stmt = true +resharper_indent_nested_while_stmt = true +resharper_indent_primary_constructor_decl_pars = inside +resharper_indent_raw_literal_string = indent +resharper_max_initializer_elements_on_line = 1 +resharper_use_heuristics_for_body_style = true +resharper_wrap_before_primary_constructor_declaration_lpar = true +resharper_wrap_object_and_collection_initializer_style = chop_if_long + +# ReSharper inspection severities +resharper_arrange_accessor_owner_body_highlighting = hint +resharper_arrange_constructor_or_destructor_body_highlighting = hint +resharper_arrange_method_or_operator_body_highlighting = hint + # Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +[*.{csproj,vbproj,fsproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 2 [*.{csproj,vbproj,proj,nativeproj,locproj}] diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..c6826ec8 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,16 @@ +name: Kook.Net on Pull Request + +on: + pull_request: + branches: [ master, dev ] + +jobs: + build_and_test: + name: Build and Test + strategy: + matrix: + target: [ windows-latest, ubuntu-latest, macOS-latest ] + uses: ./.github/workflows/build-test.yml + with: + target: ${{ matrix.target }} + dotnet-version: 8.0.x diff --git a/.github/workflows/push-or-pr.yml b/.github/workflows/push.yml similarity index 86% rename from .github/workflows/push-or-pr.yml rename to .github/workflows/push.yml index a1d79fcf..b7d6718d 100644 --- a/.github/workflows/push-or-pr.yml +++ b/.github/workflows/push.yml @@ -1,10 +1,8 @@ -name: Kook.Net on Push or Pull Request +name: Kook.Net on Push on: push: branches: [ master, dev ] -# pull_request: -# branches: [ master, dev ] jobs: build_and_test: diff --git a/Kook.Net.Sample.targets b/Kook.Net.Sample.targets new file mode 100644 index 00000000..4eabc046 --- /dev/null +++ b/Kook.Net.Sample.targets @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + false + latest + NU1803 + Debug;Release + AnyCPU + + + + + + + diff --git a/Kook.Net.Test.targets b/Kook.Net.Test.targets new file mode 100644 index 00000000..1197367c --- /dev/null +++ b/Kook.Net.Test.targets @@ -0,0 +1,37 @@ + + + + net8.0 + enable + false + Kook.Net.Tests + latest + NU1803 + Debug;Release + AnyCPU + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Kook.Net.sln b/Kook.Net.sln index bdf7bb03..86d4c285 100644 --- a/Kook.Net.sln +++ b/Kook.Net.sln @@ -35,6 +35,15 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "SolutionItems", "{55F21D65-BA1A-4A4B-B370-FD2CA0204EB2}" ProjectSection(SolutionItems) = preProject Kook.Net.targets = Kook.Net.targets + .editorconfig = .editorconfig + .gitignore = .gitignore + global.json = global.json + CHANGELOG.md = CHANGELOG.md + LICENSE = LICENSE + README.md = README.md + THIRD-PARTY-NOTICES.txt = THIRD-PARTY-NOTICES.txt + Kook.Net.Test.targets = Kook.Net.Test.targets + Kook.Net.Sample.targets = Kook.Net.Sample.targets EndProjectSection EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Kook.Net.Samples.VisualBasic", "samples\Kook.Net.Samples.VisualBasic\Kook.Net.Samples.VisualBasic.vbproj", "{43C91115-7D09-4A28-95CD-380C3B00EAE0}" diff --git a/Kook.Net.targets b/Kook.Net.targets index 5e50fa15..f86da129 100644 --- a/Kook.Net.targets +++ b/Kook.Net.targets @@ -1,7 +1,7 @@ enable - disable + enable 0.7.0 false false @@ -26,6 +26,7 @@ true true + $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) @@ -38,6 +39,7 @@ + diff --git a/README.md b/README.md index 33348b12..5c34cb64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ logo -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gehongyan/Kook.Net/push-or-pr.yml?branch=master) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gehongyan/Kook.Net/push.yml?branch=master) ![GitHub Top Language](https://img.shields.io/github/languages/top/gehongyan/Kook.Net) [![Nuget Version](https://img.shields.io/nuget/v/Kook.Net)](https://www.nuget.org/packages/Kook.Net) [![Nuget](https://img.shields.io/nuget/dt/Kook.Net?color=%230099ff)](https://www.nuget.org/packages/Kook.Net) diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 1fd721ff..b30485e9 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,5 +1,5 @@ Kook.Net uses third-party libraries or other resources that may be -distributed under licenses different than the Kook.Net. +distributed under licenses different from the Kook.Net. In the event that we accidentally failed to list a required notice, please bring it to our attention. Post an issue or email us: @@ -94,6 +94,31 @@ Licensed under the Apache License, Version 2.0. Available at https://github.com/fluentassertions/fluentassertions/blob/develop/LICENSE +License notice for Fluid +------------------------------------ + +MIT License + +Copyright (c) 2017 Sébastien Ros + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + License notice for Microsoft.NET.Test.Sdk ------------------------------------ @@ -157,6 +182,31 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [ https://www.opensource.org/licenses/bsd-license.php ] +License notice for PolySharp +------------------------------------ + +MIT License + +Copyright (c) 2022 Sergio Pedri + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + License notice for Serilog ------------------------------------ diff --git a/docs/_template/material/public/main.js b/docs/_template/material/public/main.js index a98cf9dc..24cfbe71 100644 --- a/docs/_template/material/public/main.js +++ b/docs/_template/material/public/main.js @@ -20,6 +20,9 @@ { // Ugly hack to improve toc filter. let target = document.getElementById("toc"); + + if(!target) return; + let config = { attributes: false, childList: true, subtree: true }; let observer = new MutationObserver((list) => { diff --git a/samples/Kook.Net.Samples.Audio/Kook.Net.Samples.Audio.csproj b/samples/Kook.Net.Samples.Audio/Kook.Net.Samples.Audio.csproj index 84e12802..8a9ab082 100644 --- a/samples/Kook.Net.Samples.Audio/Kook.Net.Samples.Audio.csproj +++ b/samples/Kook.Net.Samples.Audio/Kook.Net.Samples.Audio.csproj @@ -1,23 +1,12 @@ - + - - Exe - net8.0 - enable - disable - false - latestmajor - Kook.Net.Samples.Audio - NU1803 - - - - - + + + diff --git a/samples/Kook.Net.Samples.Audio/Modules/MusicModule.cs b/samples/Kook.Net.Samples.Audio/Modules/MusicModule.cs index 8737b576..884113ba 100644 --- a/samples/Kook.Net.Samples.Audio/Modules/MusicModule.cs +++ b/samples/Kook.Net.Samples.Audio/Modules/MusicModule.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Json; using System.Text.Json; using System.Text.RegularExpressions; using Kook.Audio; @@ -38,13 +39,19 @@ public async Task JoinAsync() } SocketVoiceChannel voiceChannel = voiceChannels.First(); - if (voiceChannel.ConnectedUsers.Contains(Context.Guild.CurrentUser)) + if (voiceChannel.ConnectedUsers.Contains(Context.Guild?.CurrentUser)) { await ReplyTextAsync("I'm already connected to this voice channel."); return; } - IAudioClient audioClient = await voiceChannel.ConnectAsync(); + IAudioClient? audioClient = await voiceChannel.ConnectAsync(); + if (audioClient is null) + { + await ReplyTextAsync("Failed to connect to the voice channel."); + return; + } + _musicService.SetAudioClient(Context.Channel, audioClient); await ReplyTextAsync($"Connected to {voiceChannel.Name}."); } @@ -53,7 +60,7 @@ public async Task JoinAsync() [RequireContext(ContextType.Guild)] public async Task LeaveAsync() { - if (Context.Guild.AudioClient?.ConnectionState != ConnectionState.Connected) + if (Context.Guild?.AudioClient?.ConnectionState != ConnectionState.Connected) { await ReplyTextAsync("I'm not connected to a voice channel."); return; @@ -123,14 +130,14 @@ public async Task LeaveAsync() [RequireContext(ContextType.Guild)] public async Task AddAsync([Remainder] string url) { - if (Context.Guild.AudioClient?.ConnectionState != ConnectionState.Connected) + if (Context.Guild?.AudioClient?.ConnectionState != ConnectionState.Connected) { await ReplyTextAsync("I'm not connected to a voice channel."); return; } string rawContent = markdownRegex.Replace(url, "$1"); - Uri parsed = await ConvertUriAsync(rawContent); + Uri? parsed = await ConvertUriAsync(rawContent); if (parsed is null) { await ReplyTextAsync("Invalid URL."); @@ -145,7 +152,7 @@ public async Task AddAsync([Remainder] string url) [RequireContext(ContextType.Guild)] public async Task SkipAsync() { - if (Context.Guild.AudioClient?.ConnectionState != ConnectionState.Connected) + if (Context.Guild?.AudioClient?.ConnectionState != ConnectionState.Connected) { await ReplyTextAsync("I'm not connected to a voice channel."); return; @@ -159,7 +166,7 @@ public async Task SkipAsync() [RequireContext(ContextType.Guild)] public async Task ListAsync() { - if (Context.Guild.AudioClient?.ConnectionState != ConnectionState.Connected) + if (Context.Guild?.AudioClient?.ConnectionState != ConnectionState.Connected) { await ReplyTextAsync("I'm not connected to a voice channel."); return; @@ -171,7 +178,7 @@ public async Task ListAsync() private static readonly Regex neteaseSongPageRegex = new(@"^https://music.163.com/#/song\?id=(?\d+)$", RegexOptions.Compiled); private static readonly Regex neteaseSongDirectRegex = new(@"^https://music.163.com/song/media/outer/url\?id=(?\d+).mp3$", RegexOptions.Compiled); private static readonly Regex qqSongPageRegex = new(@"^https://y.qq.com/n/ryqq/songDetail/(?\w+)$", RegexOptions.Compiled); - private async Task ConvertUriAsync(string url) + private async Task ConvertUriAsync(string url) { // 网易云音乐歌曲页面 Match match = neteaseSongPageRegex.Match(url); @@ -195,8 +202,9 @@ private async Task ConvertUriAsync(string url) { string id = match.Groups["id"].Value; HttpClient httpClient = _httpClientFactory.CreateClient("Music"); - JsonDocument response = await httpClient.GetFromJsonAsync( + JsonDocument? response = await httpClient.GetFromJsonAsync( $$$"""https://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"req":{"module":"CDN.SrfCdnDispatchServer","method":"GetCdnDispatch","param":{"guid":"8348972662","calltype":0,"userip":""}},"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"8348972662","songmid":["{{{id}}}"],"songtype":[1],"uin":"0","loginflag":1,"platform":"20"}},"comm":{"uin":0,"format":"json","ct":24,"cv":0}}"""); + if (response is null) return null; List sips = response.RootElement .GetProperty("req").GetProperty("data").GetProperty("sip") .EnumerateArray() diff --git a/samples/Kook.Net.Samples.Audio/Program.cs b/samples/Kook.Net.Samples.Audio/Program.cs index 32f21015..37d9d54a 100644 --- a/samples/Kook.Net.Samples.Audio/Program.cs +++ b/samples/Kook.Net.Samples.Audio/Program.cs @@ -3,6 +3,8 @@ using Kook.Net.Samples.Audio.Modules; using Kook.Net.Samples.Audio.Services; using Kook.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; // 这是一个使用 Kook.Net 的文本命令框架的简单示例 @@ -12,7 +14,7 @@ // 您可以在以下位置找到使用命令框架的文档: // - https://kooknet.dev/guides/text_commands/intro.html -HostApplicationBuilder builder = new(); +HostApplicationBuilder builder = Host.CreateEmptyApplicationBuilder(null); builder.Services.AddSingleton(_ => new KookSocketClient(new KookSocketConfig { LogLevel = LogSeverity.Debug, @@ -23,8 +25,7 @@ { DefaultRunMode = RunMode.Async })); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(provider => provider.GetRequiredService()); +builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => provider.GetRequiredService()); builder.Services.AddHttpClient("Music"); diff --git a/samples/Kook.Net.Samples.Audio/Services/CommandHandlingService.cs b/samples/Kook.Net.Samples.Audio/Services/CommandHandlingService.cs index fee9c6db..4c5b642f 100644 --- a/samples/Kook.Net.Samples.Audio/Services/CommandHandlingService.cs +++ b/samples/Kook.Net.Samples.Audio/Services/CommandHandlingService.cs @@ -1,6 +1,7 @@ using System.Reflection; using Kook.Commands; using Kook.WebSocket; +using Microsoft.Extensions.Hosting; namespace Kook.Net.Samples.Audio.Services; @@ -24,9 +25,12 @@ public CommandHandlingService(IServiceProvider services, CommandService commands _kook.DirectMessageReceived += MessageReceivedAsync; } - public async Task InitializeAsync() => + public async Task InitializeAsync() + { // Register modules that are public and inherit ModuleBase. - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + if (Assembly.GetEntryAssembly() is not { } entryAssembly) return; + await _commands.AddModulesAsync(entryAssembly, _services); + } public async Task MessageReceivedAsync(SocketMessage rawMessage, SocketUser user, ISocketMessageChannel channel) { @@ -50,7 +54,7 @@ public async Task MessageReceivedAsync(SocketMessage rawMessage, SocketUser user // we will handle the result in CommandExecutedAsync, } - public async Task CommandExecutedAsync(CommandInfo command, ICommandContext context, Commands.IResult result) + public async Task CommandExecutedAsync(CommandInfo? command, ICommandContext context, Commands.IResult result) { // command is unspecified when there was a search failure (command not found); we don't care about these errors if (command is null) return; diff --git a/samples/Kook.Net.Samples.Audio/Services/KookClientService.cs b/samples/Kook.Net.Samples.Audio/Services/KookClientService.cs index a2dcb072..40a56bc9 100644 --- a/samples/Kook.Net.Samples.Audio/Services/KookClientService.cs +++ b/samples/Kook.Net.Samples.Audio/Services/KookClientService.cs @@ -1,5 +1,6 @@ using Kook.Commands; using Kook.WebSocket; +using Microsoft.Extensions.Hosting; namespace Kook.Net.Samples.Audio.Services; @@ -24,7 +25,8 @@ private static Task LogAsync(LogMessage log) /// public async Task StartAsync(CancellationToken cancellationToken) { - string token = Environment.GetEnvironmentVariable("KookDebugToken", EnvironmentVariableTarget.User); + string token = Environment.GetEnvironmentVariable("KookDebugToken", EnvironmentVariableTarget.User) + ?? throw new ArgumentNullException("KookDebugToken"); await _socketClient.LoginAsync(TokenType.Bot, token); await _socketClient.StartAsync(); } diff --git a/samples/Kook.Net.Samples.Audio/Services/MusicService.cs b/samples/Kook.Net.Samples.Audio/Services/MusicService.cs index dfe499f1..47f35545 100644 --- a/samples/Kook.Net.Samples.Audio/Services/MusicService.cs +++ b/samples/Kook.Net.Samples.Audio/Services/MusicService.cs @@ -2,25 +2,20 @@ using System.Diagnostics; using Kook.Audio; using Kook.WebSocket; +using Microsoft.Extensions.Hosting; namespace Kook.Net.Samples.Audio.Services; public class MusicService : IHostedService { - private readonly BlockingCollection _musicQueue; - private Uri _currentSong; - private ISocketMessageChannel _sourceChannel; - private IAudioClient _audioClient; - private Process _ffmpeg; - - public MusicService() - { - _musicQueue = new BlockingCollection(); - } + private readonly BlockingCollection _musicQueue = []; + private ISocketMessageChannel? _sourceChannel; + private IAudioClient? _audioClient; + private Process? _ffmpeg; public IEnumerable Queue => _musicQueue; - public Uri CurrentPlaying => _currentSong; + public Uri? CurrentPlaying { get; private set; } public void SetAudioClient(ISocketMessageChannel sourceChannel, IAudioClient audioClient) { @@ -40,7 +35,7 @@ public void Skip() public async Task StartAsync(CancellationToken cancellationToken) { - while (_musicQueue.TryTake(out Uri source, Timeout.Infinite, cancellationToken)) + while (_musicQueue.TryTake(out Uri? source, Timeout.Infinite, cancellationToken)) await PlayAsync(source, cancellationToken); } @@ -54,11 +49,12 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task PlayAsync(Uri source, CancellationToken cancellationToken) { - if (_audioClient?.ConnectionState != ConnectionState.Connected) + if (_audioClient?.ConnectionState != ConnectionState.Connected + || _sourceChannel is null) return; - _currentSong = source; + CurrentPlaying = source; _ = _sourceChannel.SendTextAsync($"Now playing {source}"); - using Process ffmpeg = Process.Start(new ProcessStartInfo + using Process? ffmpeg = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $"""-hide_banner -loglevel panic -i "{source}" -ac 2 -f s16le -ar 48000 pipe:1""", @@ -67,8 +63,8 @@ private async Task PlayAsync(Uri source, CancellationToken cancellationToken) }); _ffmpeg = ffmpeg; if (ffmpeg is null) return; - await using var output = ffmpeg.StandardOutput.BaseStream; - await using var kook = _audioClient.CreatePcmStream(AudioApplication.Voice); + await using Stream output = ffmpeg.StandardOutput.BaseStream; + await using AudioOutStream kook = _audioClient.CreatePcmStream(AudioApplication.Voice); try { await output.CopyToAsync(kook, cancellationToken); diff --git a/samples/Kook.Net.Samples.CardMarkup/Extensions/TemplateExtensions.cs b/samples/Kook.Net.Samples.CardMarkup/Extensions/TemplateExtensions.cs index 441eb1ca..d504ddbc 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Extensions/TemplateExtensions.cs +++ b/samples/Kook.Net.Samples.CardMarkup/Extensions/TemplateExtensions.cs @@ -4,8 +4,6 @@ using Kook.Net.Samples.CardMarkup.Models; using Kook.Net.Samples.CardMarkup.Models.Template; -// ReSharper disable SuggestVarOrType_BuiltInTypes - namespace Kook.Net.Samples.CardMarkup.Extensions; public static class TemplateExtensions @@ -14,55 +12,43 @@ public static class TemplateExtensions public static async Task?> RenderVoteAsync(this Vote vote) { - if (!await LoadVoteTemplateAsync()) - { - return null; - } + if (!await LoadVoteTemplateAsync()) return null; - var templateOptions = new TemplateOptions(); + TemplateOptions templateOptions = new(); templateOptions.MemberAccessStrategy.Register(); templateOptions.MemberAccessStrategy.Register(); templateOptions.MemberAccessStrategy.Register(); - var context = new TemplateContext(vote, templateOptions); - var result = await _voteTemplate!.RenderAsync(context, HtmlEncoder.Default); - if (result is null) - { - return null; - } + TemplateContext context = new(vote, templateOptions); + string result = await _voteTemplate.RenderAsync(context, HtmlEncoder.Default); + if (string.IsNullOrWhiteSpace(result)) return null; return await CardMarkupSerializer.DeserializeAsync(result); } - public static async Task?> RenderBigCard() + public static async Task> RenderBigCard() { - var source = await File.ReadAllTextAsync("Cards/big-card.xml"); - var cards = await CardMarkupSerializer.DeserializeAsync(source); + string source = await File.ReadAllTextAsync("Cards/big-card.xml"); + IEnumerable cards = await CardMarkupSerializer.DeserializeAsync(source); return cards; } - public static async Task?> RenderMultipleCards() + public static async Task> RenderMultipleCards() { - var source = await File.ReadAllTextAsync("Cards/multiple-cards.xml"); - var cards = await CardMarkupSerializer.DeserializeAsync(source); + string source = await File.ReadAllTextAsync("Cards/multiple-cards.xml"); + IEnumerable cards = await CardMarkupSerializer.DeserializeAsync(source); return cards; } private static async Task LoadVoteTemplateAsync() { - if (_voteTemplate is not null) - { - return true; - } + if (_voteTemplate is not null) return true; - var source = await File.ReadAllTextAsync("Cards/vote.xml.liquid"); - var parser = new FluidParser(); - var result = parser.TryParse(source, out var template); + string source = await File.ReadAllTextAsync("Cards/vote.xml.liquid"); + FluidParser parser = new(); + bool result = parser.TryParse(source, out IFluidTemplate? template); - if (!result) - { - return false; - } + if (!result) return false; _voteTemplate = template; return true; diff --git a/samples/Kook.Net.Samples.CardMarkup/Kook.Net.Samples.CardMarkup.csproj b/samples/Kook.Net.Samples.CardMarkup/Kook.Net.Samples.CardMarkup.csproj index 48212c33..b70e0958 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Kook.Net.Samples.CardMarkup.csproj +++ b/samples/Kook.Net.Samples.CardMarkup/Kook.Net.Samples.CardMarkup.csproj @@ -1,22 +1,9 @@  - - Exe - net8.0 - enable - enable - false - latestmajor - Kook.Net.Samples.CardMarkup - NU1803 - + - - - - - + diff --git a/samples/Kook.Net.Samples.CardMarkup/Modules/CardCommandModule.cs b/samples/Kook.Net.Samples.CardMarkup/Modules/CardCommandModule.cs index d710e65d..56f50882 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Modules/CardCommandModule.cs +++ b/samples/Kook.Net.Samples.CardMarkup/Modules/CardCommandModule.cs @@ -10,7 +10,7 @@ public class CardCommandModule : ModuleBase [Command("vote")] public async Task SendTestVoteCard() { - var allUsers = new List + List allUsers = new List { new() { Avatar = "https://img.kaiheila.cn/avatars/2020-09/ov2wQ8r2qZ0dc07i.gif/icon" }, new() { Avatar = "https://img.kaiheila.cn/avatars/2020-11/LjtEMkmH3U0hs0hs.jpg/icon" }, @@ -23,7 +23,7 @@ public async Task SendTestVoteCard() new() { Avatar = "https://img.kaiheila.cn/avatars/2020-06/SbFPjoBb5202s02s.jpg/icon" } }; - var model = new Vote + Vote model = new Vote { Title = "朋友们,今晚开黑玩什么游戏?", Games = @@ -49,11 +49,12 @@ public async Task SendTestVoteCard() ] }; - var cards = await model.RenderVoteAsync(); + IEnumerable? cards = await model.RenderVoteAsync(); if (cards is null) { await ReplyTextAsync("Failed to render cards, check the logs."); + return; } await ReplyCardsAsync(cards); @@ -62,14 +63,14 @@ public async Task SendTestVoteCard() [Command("big-card")] public async Task SendBigCard() { - var cards = await TemplateExtensions.RenderBigCard(); + IEnumerable cards = await TemplateExtensions.RenderBigCard(); await ReplyCardsAsync(cards); } [Command("multiple")] public async Task SendMultipleCards() { - var cards = await TemplateExtensions.RenderMultipleCards(); + IEnumerable cards = await TemplateExtensions.RenderMultipleCards(); await ReplyCardsAsync(cards); } } diff --git a/samples/Kook.Net.Samples.CardMarkup/Program.cs b/samples/Kook.Net.Samples.CardMarkup/Program.cs index 9ad7477b..7c3fc28f 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Program.cs +++ b/samples/Kook.Net.Samples.CardMarkup/Program.cs @@ -23,7 +23,7 @@ // 示例文件 Cards/multiple-cards.xml 在一个卡片消息中定义了多个卡片 // 使用 !multiple 命令在 Kook 中发送此卡片消息 -var builder = Host.CreateApplicationBuilder(args); +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Configuration.AddInMemoryCollection(new[] { @@ -40,7 +40,7 @@ }); builder.Services.AddSingleton(sp => { - var config = sp.GetRequiredService(); + KookSocketConfig config = sp.GetRequiredService(); return new KookSocketClient(config); }); builder.Services.AddSingleton(); @@ -48,6 +48,6 @@ builder.Services.AddSingleton(); builder.Services.AddHostedService(); -var app = builder.Build(); +IHost app = builder.Build(); app.Run(); diff --git a/samples/Kook.Net.Samples.CardMarkup/Services/CommandHandlerService.cs b/samples/Kook.Net.Samples.CardMarkup/Services/CommandHandlerService.cs index d3557107..059b758c 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Services/CommandHandlerService.cs +++ b/samples/Kook.Net.Samples.CardMarkup/Services/CommandHandlerService.cs @@ -23,33 +23,24 @@ public CommandHandlerService(CommandService commandService, KookSocketClient soc public async Task InitializeAsync() { - await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _serviceProvider); + if (Assembly.GetEntryAssembly() is not { } assembly) return; + await _commandService.AddModulesAsync(assembly, _serviceProvider); } private async Task MessageReceivedAsync(SocketMessage rawMessage, SocketUser user, ISocketMessageChannel channel) { - if (rawMessage is not SocketUserMessage { Source: MessageSource.User } message) - { - return; - } + if (rawMessage is not SocketUserMessage { Source: MessageSource.User } message) return; int argPos = 0; - if (!message.HasCharPrefix('!', ref argPos)) - { - return; - } + if (!message.HasCharPrefix('!', ref argPos)) return; SocketCommandContext context = new(_socketClient, message); await _commandService.ExecuteAsync(context, argPos, _serviceProvider); } - private static async Task CommandExecutedAsync(CommandInfo command, ICommandContext context, IResult result) + private static async Task CommandExecutedAsync(CommandInfo? command, ICommandContext context, IResult result) { - if (result.IsSuccess) - { - return; - } - + if (result.IsSuccess) return; await context.Channel.SendTextAsync($"error: {result}"); } } diff --git a/samples/Kook.Net.Samples.CardMarkup/Services/KookSocketService.cs b/samples/Kook.Net.Samples.CardMarkup/Services/KookSocketService.cs index 58842020..9345ea1b 100644 --- a/samples/Kook.Net.Samples.CardMarkup/Services/KookSocketService.cs +++ b/samples/Kook.Net.Samples.CardMarkup/Services/KookSocketService.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -// ReSharper disable SuggestVarOrType_BuiltInTypes - namespace Kook.Net.Samples.CardMarkup.Services; public class KookSocketService : IHostedService @@ -40,7 +38,8 @@ public async Task StartAsync(CancellationToken cancellationToken) await Task.CompletedTask; }; - var token = Environment.GetEnvironmentVariable("KookDebugToken"); + string token = Environment.GetEnvironmentVariable("KookDebugToken") + ?? throw new ArgumentNullException("KookDebugToken"); await _socketClient.LoginAsync(TokenType.Bot, token); await _socketClient.StartAsync(); diff --git a/samples/Kook.Net.Samples.Docker/Kook.Net.Samples.Docker.csproj b/samples/Kook.Net.Samples.Docker/Kook.Net.Samples.Docker.csproj index 69b6e4fe..0ecb2255 100644 --- a/samples/Kook.Net.Samples.Docker/Kook.Net.Samples.Docker.csproj +++ b/samples/Kook.Net.Samples.Docker/Kook.Net.Samples.Docker.csproj @@ -1,21 +1,15 @@ - - Exe - net8.0 - enable - false - Linux - + - - - + + Linux + - - - .dockerignore - - + + + .dockerignore + + diff --git a/samples/Kook.Net.Samples.Docker/Program.cs b/samples/Kook.Net.Samples.Docker/Program.cs index c9d5d08e..fd4966b0 100644 --- a/samples/Kook.Net.Samples.Docker/Program.cs +++ b/samples/Kook.Net.Samples.Docker/Program.cs @@ -124,10 +124,9 @@ async Task MessageReceivedAsync(SocketMessage message, SocketTextChannel channel) { // Bot 永远不应该响应自己的消息 - if (author.Id == client.CurrentUser.Id) + if (author.Id == client.CurrentUser?.Id) return; - if (message.Content == "!ping") { // 创建一个 CardBuilder,卡片将会包含一个文本模块和一个按钮模块 @@ -156,7 +155,7 @@ async Task MessageButtonClickedAsync(string value, // 检查按钮的值是否为之前的代码中设置的值 if (value == "unique-id") { - IMessage messageEntity = await message.GetOrDownloadAsync(); + IMessage? messageEntity = await message.GetOrDownloadAsync(); if (messageEntity is IUserMessage userMessage) await userMessage.ReplyTextAsync("按钮被点击了!", isQuote: true); } diff --git a/samples/Kook.Net.Samples.FSharp/Kook.Net.Samples.FSharp.fsproj b/samples/Kook.Net.Samples.FSharp/Kook.Net.Samples.FSharp.fsproj index f8727df3..a9453d6e 100644 --- a/samples/Kook.Net.Samples.FSharp/Kook.Net.Samples.FSharp.fsproj +++ b/samples/Kook.Net.Samples.FSharp/Kook.Net.Samples.FSharp.fsproj @@ -1,17 +1,9 @@  - - Exe - net8.0 - false - + - - - - - - - + + + diff --git a/samples/Kook.Net.Samples.ReactionRoleBot/Configurations/KookBotConfigurations.cs b/samples/Kook.Net.Samples.ReactionRoleBot/Configurations/KookBotConfigurations.cs index 3a75c84f..68da99f8 100644 --- a/samples/Kook.Net.Samples.ReactionRoleBot/Configurations/KookBotConfigurations.cs +++ b/samples/Kook.Net.Samples.ReactionRoleBot/Configurations/KookBotConfigurations.cs @@ -7,17 +7,17 @@ public class KookBotConfigurations /// /// Token /// - public string Token { get; init; } + public string Token { get; init; } = string.Empty; /// /// 频道ID列表 /// - public KookChannels Channels { get; init; } + public KookChannels Channels { get; init; } = null!; /// /// 角色ID列表 /// - public KookRoles Roles { get; init; } + public KookRoles Roles { get; init; } = null!; /// /// KOOK Bot运行环境 diff --git a/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtension.cs b/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtension.cs index 04045419..ccfa0e85 100644 --- a/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtension.cs +++ b/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtension.cs @@ -7,26 +7,20 @@ namespace Kook.Net.Samples.ReactionRoleBot.Extensions; public partial class KookBotClientExtension : IHostedService { - private readonly IServiceProvider _service; private readonly KookBotConfigurations _kookBotConfigurations; private readonly KookSocketClient _kookSocketClient; private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; public KookBotClientExtension( - IServiceProvider service, KookBotConfigurations kookBotConfigurations, KookSocketClient kookSocketClient, - ILogger logger, IHttpClientFactory httpClientFactory) + ILogger logger) { - _service = service; _kookBotConfigurations = kookBotConfigurations; _kookSocketClient = kookSocketClient; _logger = logger; - _httpClientFactory = httpClientFactory; _kookSocketClient.Log += LogHandler; - // _kookSocketClient.Ready += SendReactionCard; _kookSocketClient.ReactionAdded += ProcessReactionRoleAdd; _kookSocketClient.ReactionRemoved += ProcessReactionRoleRemove; } diff --git a/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtensionEventHandlers.cs b/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtensionEventHandlers.cs index 07c33198..2104d6e6 100644 --- a/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtensionEventHandlers.cs +++ b/samples/Kook.Net.Samples.ReactionRoleBot/Extensions/KookBotClientExtensionEventHandlers.cs @@ -11,8 +11,10 @@ private async Task ProcessReactionRoleAdd(Cacheable message, Soc if (!reaction.Emote.Equals(Emoji.Parse(":computer:"))) return; - SocketRole socketRole = _kookSocketClient.GetGuild(1591057729615250).GetRole(3001653); - SocketGuildUser socketGuildUser = (SocketGuildUser)reaction.User; + SocketRole? socketRole = _kookSocketClient.GetGuild(1591057729615250)?.GetRole(3001653); + SocketGuildUser? socketGuildUser = reaction.User as SocketGuildUser; + + if (socketGuildUser == null || socketRole == null) return; await socketGuildUser.AddRoleAsync(socketRole); @@ -25,12 +27,12 @@ private async Task ProcessReactionRoleAdd(Cacheable message, Soc .AddModule(new SectionModuleBuilder() .WithText($"已在服务器 `{socketGuildUser.Guild.Name}` 内为您授予角色 `{socketRole.Name}`", true)) .AddModule(new ContextModuleBuilder().AddElement(new KMarkdownElementBuilder() - .WithContent($"{_kookSocketClient.CurrentUser.Username} | {time}"))); + .WithContent($"{_kookSocketClient.CurrentUser?.Username} | {time}"))); await dmChannel.SendCardAsync(builder.Build()); - _logger.Information("{User}#{IdentifyNumber} is granted {RoleName}", - socketGuildUser.Username, socketGuildUser.IdentifyNumber, socketRole.Name); + _logger.Information("{User} is granted {RoleName}", + socketGuildUser.UsernameAndIdentifyNumber(false), socketRole.Name); } private async Task ProcessReactionRoleRemove(Cacheable message, SocketTextChannel channel, @@ -40,8 +42,10 @@ private async Task ProcessReactionRoleRemove(Cacheable message, if (!reaction.Emote.Equals(Emoji.Parse(":computer:"))) return; - SocketRole socketRole = _kookSocketClient.GetGuild(1591057729615250).GetRole(3001653); - SocketGuildUser socketGuildUser = (SocketGuildUser)reaction.User; + SocketRole? socketRole = _kookSocketClient.GetGuild(1591057729615250)?.GetRole(3001653); + SocketGuildUser? socketGuildUser = reaction.User as SocketGuildUser; + + if (socketGuildUser == null || socketRole == null) return; await socketGuildUser.RemoveRoleAsync(socketRole); @@ -54,12 +58,12 @@ private async Task ProcessReactionRoleRemove(Cacheable message, .AddModule(new SectionModuleBuilder() .WithText($"已在服务器 `{socketGuildUser.Guild.Name}` 内为您撤销角色 `{socketRole.Name}`", true)) .AddModule(new ContextModuleBuilder().AddElement(new KMarkdownElementBuilder() - .WithContent($"{_kookSocketClient.CurrentUser.Username} | {time}"))); + .WithContent($"{_kookSocketClient.CurrentUser?.Username} | {time}"))); await dmChannel.SendCardAsync(builder.Build()); - _logger.Information("{User}#{IdentifyNumber} is revoked {RoleName}", - socketGuildUser.Username, socketGuildUser.IdentifyNumber, socketRole.Name); + _logger.Information("{User} is revoked {RoleName}", + socketGuildUser.UsernameAndIdentifyNumber(false), socketRole.Name); } private async Task SendReactionCard() @@ -69,8 +73,8 @@ private async Task SendReactionCard() .WithSize(CardSize.Large) .AddModule(new HeaderModuleBuilder().WithText("互动角色")) .AddModule(new SectionModuleBuilder().WithText("点击下方回应以获取/移除角色\n:computer: 开发者", true)); - Cacheable response = await _kookSocketClient.GetGuild(1591057729615250).GetTextChannel(5770952608991958) - .SendCardAsync(builder.Build()).ConfigureAwait(false); + if (_kookSocketClient.GetGuild(1591057729615250)?.GetTextChannel(5770952608991958) is not { } channel) return; + Cacheable response = await channel.SendCardAsync(builder.Build()).ConfigureAwait(false); await _kookSocketClient.Rest.AddReactionAsync(response.Id, Emoji.Parse(":computer:")); } } diff --git a/samples/Kook.Net.Samples.ReactionRoleBot/Kook.Net.Samples.ReactionRoleBot.csproj b/samples/Kook.Net.Samples.ReactionRoleBot/Kook.Net.Samples.ReactionRoleBot.csproj index 08e85a67..5147b574 100644 --- a/samples/Kook.Net.Samples.ReactionRoleBot/Kook.Net.Samples.ReactionRoleBot.csproj +++ b/samples/Kook.Net.Samples.ReactionRoleBot/Kook.Net.Samples.ReactionRoleBot.csproj @@ -1,26 +1,13 @@ - - Exe - net8.0 - enable - disable - false - latestmajor - Kook.Net.Samples.ReactionRoleBot - NU1803 - - - - - + - + - + diff --git a/samples/Kook.Net.Samples.SimpleBot/Kook.Net.Samples.SimpleBot.csproj b/samples/Kook.Net.Samples.SimpleBot/Kook.Net.Samples.SimpleBot.csproj index 4070d4f9..66ee3368 100644 --- a/samples/Kook.Net.Samples.SimpleBot/Kook.Net.Samples.SimpleBot.csproj +++ b/samples/Kook.Net.Samples.SimpleBot/Kook.Net.Samples.SimpleBot.csproj @@ -1,18 +1,5 @@ - - Exe - net8.0 - enable - enable - false - latestmajor - Kook.Net.Samples.SimpleBot - NU1803 - - - - - + diff --git a/samples/Kook.Net.Samples.SimpleBot/Program.cs b/samples/Kook.Net.Samples.SimpleBot/Program.cs index 672a667a..9d5e6683 100644 --- a/samples/Kook.Net.Samples.SimpleBot/Program.cs +++ b/samples/Kook.Net.Samples.SimpleBot/Program.cs @@ -130,7 +130,7 @@ async Task MessageReceivedAsync(SocketMessage message, SocketTextChannel channel) { // Bot 永远不应该响应自己的消息 - if (author.Id == client.CurrentUser.Id) + if (author.Id == client.CurrentUser?.Id) return; @@ -162,7 +162,7 @@ async Task MessageButtonClickedAsync(string value, // 检查按钮的值是否为之前的代码中设置的值 if (value == "unique-id") { - IMessage messageEntity = await message.GetOrDownloadAsync(); + IMessage? messageEntity = await message.GetOrDownloadAsync(); if (messageEntity is IUserMessage userMessage) await userMessage.ReplyTextAsync("按钮被点击了!", isQuote: true); } diff --git a/samples/Kook.Net.Samples.TextCommands/Kook.Net.Samples.TextCommands.csproj b/samples/Kook.Net.Samples.TextCommands/Kook.Net.Samples.TextCommands.csproj index b09dd730..2b31c6f8 100644 --- a/samples/Kook.Net.Samples.TextCommands/Kook.Net.Samples.TextCommands.csproj +++ b/samples/Kook.Net.Samples.TextCommands/Kook.Net.Samples.TextCommands.csproj @@ -1,23 +1,12 @@ - - Exe - net8.0 - enable - disable - false - latestmajor - Kook.Net.Samples.TextCommands - NU1803 - - - - - + + + diff --git a/samples/Kook.Net.Samples.TextCommands/Modules/PublicModule.cs b/samples/Kook.Net.Samples.TextCommands/Modules/PublicModule.cs index e5f0699d..f14b8149 100644 --- a/samples/Kook.Net.Samples.TextCommands/Modules/PublicModule.cs +++ b/samples/Kook.Net.Samples.TextCommands/Modules/PublicModule.cs @@ -9,12 +9,12 @@ namespace Kook.Net.Samples.TextCommands.Modules; public class PublicModule : ModuleBase { // Dependency Injection will fill this value in for us - public PictureService PictureService { get; set; } + public required PictureService PictureService { get; set; } [Command("ping")] [Alias("pong", "hello")] - public Task PingAsync() - => ReplyTextAsync("pong!"); + public Task PingAsync() => + ReplyTextAsync("pong!"); [Command("cat")] public async Task CatAsync() @@ -28,15 +28,15 @@ public async Task CatAsync() // Get info on a user, or the user who invoked the command if one is not specified [Command("userinfo")] - public async Task UserInfoAsync(IUser user = null) + public async Task UserInfoAsync(IUser? user = null) { user ??= Context.User; - - await ReplyTextAsync(user.ToString()); + await ReplyTextAsync(user.ToString() ?? user.Username); } [Command("say")] - public async Task Emoji(string text) => await Context.Message.AddReactionAsync(new Emoji("\uD83D\uDC4C")); + public async Task Emoji(string text) => + await Context.Message.AddReactionAsync(new Emoji("\uD83D\uDC4C")); // Ban a user [Command("ban")] @@ -45,7 +45,7 @@ public async Task UserInfoAsync(IUser user = null) [RequireUserPermission(GuildPermission.BanMembers)] // make sure the bot itself can ban [RequireBotPermission(GuildPermission.BanMembers)] - public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null) + public async Task BanUserAsync(IGuildUser user, [Remainder] string? reason = null) { await user.Guild.AddBanAsync(user, reason: reason); await ReplyTextAsync("ok!"); @@ -53,36 +53,37 @@ public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null // [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space [Command("echo")] - public Task EchoAsync([Remainder] string text) + public Task EchoAsync([Remainder] string text) => // Insert a ZWSP before the text to prevent triggering other bots! - => ReplyTextAsync('\u200B' + text); + ReplyTextAsync('\u200B' + text); // 'params' will parse space-separated elements into a list [Command("list")] - public Task ListAsync(params string[] objects) - => ReplyTextAsync("You listed: " + string.Join("; ", objects)); + public Task ListAsync(params string[] objects) => + ReplyTextAsync($"You listed: {string.Join("; ", objects)}"); // Setting a custom ErrorMessage property will help clarify the precondition error [Command("guild_only")] - [RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")] - public Task GuildOnlyCommand() - => ReplyTextAsync("Nothing to see here!"); + [RequireContext(ContextType.Guild, + ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")] + public Task GuildOnlyCommand() => + ReplyTextAsync("Nothing to see here!"); [Command("per")] public async Task ModifyCategoryPermissions() { - SocketUser contextUser = Context.User; - await ((IGuildChannel)Context.Channel).AddPermissionOverwriteAsync((IGuildUser)Context.User); - await ((SocketChannel)Context.Channel).UpdateAsync(); - await Context.Guild.GetChannel(Context.Channel.Id).ModifyPermissionOverwriteAsync((IGuildUser)Context.User, - permissions => permissions.Modify(viewChannel: PermValue.Allow, sendMessages: PermValue.Deny, attachFiles: PermValue.Allow)); - } - - [Command("create")] - public async Task CreateChannel() - { - IReadOnlyCollection onlyCollection = ((SocketGuildUser)Context.User).Roles; - RestGuildUser guildUserAsync = await Context.Client.Rest.GetGuildUserAsync(7557797319758285, 1253960922); - IReadOnlyCollection readOnlyCollection = guildUserAsync.RoleIds; + if (Context.Guild is not { } guild) return; + if (Context.Channel is not IGuildChannel guildChannel) return; + await guildChannel.AddPermissionOverwriteAsync((IGuildUser)Context.User); + if (guildChannel is SocketChannel socketChannel) + await socketChannel.UpdateAsync(); + if (guild.GetChannel(Context.Channel.Id) is { } socketGuildChannel) + { + await socketGuildChannel.ModifyPermissionOverwriteAsync((IGuildUser)Context.User, + permissions => permissions.Modify( + viewChannel: PermValue.Allow, + sendMessages: PermValue.Deny, + attachFiles: PermValue.Allow)); + } } } diff --git a/samples/Kook.Net.Samples.TextCommands/Program.cs b/samples/Kook.Net.Samples.TextCommands/Program.cs index a22ebc11..d7c5c5a1 100644 --- a/samples/Kook.Net.Samples.TextCommands/Program.cs +++ b/samples/Kook.Net.Samples.TextCommands/Program.cs @@ -3,51 +3,35 @@ using Kook.Net.Samples.TextCommands.Services; using Kook.WebSocket; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; // 这是一个使用 Kook.Net 的文本命令框架的简单示例 -// 此处使用了依赖注入,不需要实现 IDisposable 接口,using 声明会为我们处理弃置模式。 +// 此处使用了 .NET 通用主机承载 KOOK Bot 服务,该主机将帮助我们管理应用程序与服务的依赖注入与生命周期。 +// 有关 .NET 通用主机的更多信息,请参阅 https://learn.microsoft.com/dotnet/core/extensions/generic-host // 如果您使用另一个依赖注入框架,应该查阅其文档以找到最佳的处理方式。 // 您可以在以下位置找到使用命令框架的文档: // - https://kooknet.dev/guides/text_commands/intro.html -await using ServiceProvider services = ConfigureServices(); -KookSocketClient client = services.GetRequiredService(); +HostApplicationBuilder builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings()); -client.Log += LogAsync; -services.GetRequiredService().Log += LogAsync; - -// 令牌(Tokens)应被视为机密数据,永远不应硬编码在代码中 -// 在实际开发中,为了保护令牌的安全性,建议将令牌存储在安全的环境中 -// 例如本地 .json、.yaml、.xml、.txt 文件、环境变量或密钥管理系统 -// 这样可以避免将敏感信息直接暴露在代码中,以防止令牌被滥用或泄露 -string token = Environment.GetEnvironmentVariable("KookDebugToken", EnvironmentVariableTarget.User); -await client.LoginAsync(TokenType.Bot, token); -await client.StartAsync(); - -// 此处初始化了命令所需的逻辑。 -await services.GetRequiredService().InitializeAsync(); -await Task.Delay(Timeout.Infinite); -return; - -// Log 事件,此处以直接输出到控制台为例 -Task LogAsync(LogMessage log) +builder.Services.AddSingleton(_ => new KookSocketConfig { - Console.WriteLine(log.ToString()); - return Task.CompletedTask; -} - -// 此处配置了依赖注入的服务 -ServiceProvider ConfigureServices() => new ServiceCollection() - .AddSingleton(_ => new KookSocketClient(new KookSocketConfig - { - AlwaysDownloadUsers = true, - LogLevel = LogSeverity.Debug, - AcceptLanguage = "en-US" - })) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); + AlwaysDownloadUsers = true, + LogLevel = LogSeverity.Debug, + AcceptLanguage = "en-US" +}); +builder.Services.AddSingleton(provider => +{ + KookSocketConfig config = provider.GetRequiredService(); + return new KookSocketClient(config); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Services.AddSingleton(); +builder.Services.AddHttpClient("Pictures"); + +IHost app = builder.Build(); +await app.RunAsync(); diff --git a/samples/Kook.Net.Samples.TextCommands/Services/CommandHandlingService.cs b/samples/Kook.Net.Samples.TextCommands/Services/CommandHandlingService.cs index 75dfe08e..922536f5 100644 --- a/samples/Kook.Net.Samples.TextCommands/Services/CommandHandlingService.cs +++ b/samples/Kook.Net.Samples.TextCommands/Services/CommandHandlingService.cs @@ -25,9 +25,12 @@ public CommandHandlingService(IServiceProvider services) _kook.DirectMessageReceived += MessageReceivedAsync; } - public async Task InitializeAsync() => + public async Task InitializeAsync() + { + if (Assembly.GetEntryAssembly() is not { } assembly) return; // Register modules that are public and inherit ModuleBase. - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + await _commands.AddModulesAsync(assembly, _services); + } public async Task MessageReceivedAsync(SocketMessage rawMessage, SocketUser user, ISocketMessageChannel channel) { @@ -51,13 +54,15 @@ public async Task MessageReceivedAsync(SocketMessage rawMessage, SocketUser user // we will handle the result in CommandExecutedAsync, } - public async Task CommandExecutedAsync(CommandInfo command, ICommandContext context, IResult result) + public async Task CommandExecutedAsync(CommandInfo? command, ICommandContext context, IResult result) { // command is unspecified when there was a search failure (command not found); we don't care about these errors - if (command is null) return; + if (command is null) + return; // the command was successful, we don't care about this result, unless we want to log that a command succeeded. - if (result.IsSuccess) return; + if (result.IsSuccess) + return; // the command failed, let's notify the user that something happened. await context.Channel.SendTextAsync($"error: {result}"); diff --git a/samples/Kook.Net.Samples.TextCommands/Services/KookClientService.cs b/samples/Kook.Net.Samples.TextCommands/Services/KookClientService.cs new file mode 100644 index 00000000..ccfcbbe8 --- /dev/null +++ b/samples/Kook.Net.Samples.TextCommands/Services/KookClientService.cs @@ -0,0 +1,53 @@ +using Kook.Commands; +using Kook.WebSocket; +using Microsoft.Extensions.Hosting; + +namespace Kook.Net.Samples.TextCommands.Services; + +public class KookClientService : IHostedService +{ + private readonly KookSocketClient _client; + private readonly CommandService _commandService; + private readonly CommandHandlingService _commandHandlingService; + + public KookClientService(KookSocketClient client, + CommandService commandService, CommandHandlingService commandHandlingService) + { + _client = client; + _commandService = commandService; + _commandHandlingService = commandHandlingService; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + _client.Log += LogAsync; + _commandService.Log += LogAsync; + + // 令牌(Tokens)应被视为机密数据,永远不应硬编码在代码中 + // 在实际开发中,为了保护令牌的安全性,建议将令牌存储在安全的环境中 + // 例如本地 .json、.yaml、.xml、.txt 文件、环境变量或密钥管理系统 + // 这样可以避免将敏感信息直接暴露在代码中,以防止令牌被滥用或泄露 + string token = Environment.GetEnvironmentVariable("KookDebugToken", EnvironmentVariableTarget.User) + ?? throw new InvalidOperationException("Token not found"); + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + await _commandHandlingService.InitializeAsync(); + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + await _client.StopAsync(); + await _client.LogoutAsync(); + } + + /// + /// Log 事件,此处以直接输出到控制台为例 + /// + private static Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + return Task.CompletedTask; + } +} diff --git a/samples/Kook.Net.Samples.TextCommands/Services/PictureService.cs b/samples/Kook.Net.Samples.TextCommands/Services/PictureService.cs index 1efa90b4..d03b5c03 100644 --- a/samples/Kook.Net.Samples.TextCommands/Services/PictureService.cs +++ b/samples/Kook.Net.Samples.TextCommands/Services/PictureService.cs @@ -1,15 +1,11 @@ namespace Kook.Net.Samples.TextCommands.Services; -public class PictureService +public class PictureService(IHttpClientFactory httpClientFactory) { - private readonly HttpClient _http; - - public PictureService(HttpClient http) - => _http = http; - public async Task GetCatPictureAsync() { - HttpResponseMessage resp = await _http.GetAsync("https://cataas.com/cat"); + HttpClient httpClient = httpClientFactory.CreateClient("Pictures"); + HttpResponseMessage resp = await httpClient.GetAsync("https://cataas.com/cat"); return await resp.Content.ReadAsStreamAsync(); } } diff --git a/samples/Kook.Net.Samples.VisualBasic/Kook.Net.Samples.VisualBasic.vbproj b/samples/Kook.Net.Samples.VisualBasic/Kook.Net.Samples.VisualBasic.vbproj index d1f738ed..66ee3368 100644 --- a/samples/Kook.Net.Samples.VisualBasic/Kook.Net.Samples.VisualBasic.vbproj +++ b/samples/Kook.Net.Samples.VisualBasic/Kook.Net.Samples.VisualBasic.vbproj @@ -1,14 +1,5 @@ - - Exe - Kook.Net.Samples.VisualBasic - net8.0 - false - - - - - + diff --git a/src/Kook.Net.CardMarkup/CardMarkupSerializer.cs b/src/Kook.Net.CardMarkup/CardMarkupSerializer.cs index ee189c17..c9becf83 100644 --- a/src/Kook.Net.CardMarkup/CardMarkupSerializer.cs +++ b/src/Kook.Net.CardMarkup/CardMarkupSerializer.cs @@ -1,12 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Xml; using Kook.CardMarkup.Extensions; using Kook.CardMarkup.Models; -#if NETSTANDARD2_1 || NET5_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -#endif - namespace Kook.CardMarkup; /// @@ -27,9 +24,9 @@ public static class CardMarkupSerializer public static async Task> DeserializeAsync(FileInfo file, CancellationToken token = default) { #if NETSTANDARD2_0 || NET462 - using var fs = file.OpenRead(); + using FileStream fs = file.OpenRead(); #else - await using var fs = file.OpenRead(); + await using FileStream fs = file.OpenRead(); #endif return await DeserializeAsync(fs, token); } @@ -44,7 +41,7 @@ public static async Task> DeserializeAsync(FileInfo file, Can /// enumerable public static async Task> DeserializeAsync(string xmlText, CancellationToken token = default) { - using var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlText)); + using MemoryStream xmlStream = new(Encoding.UTF8.GetBytes(xmlText)); return await DeserializeAsync(xmlStream, token); } @@ -58,30 +55,28 @@ public static async Task> DeserializeAsync(string xmlText, Ca /// enumerable public static async Task> DeserializeAsync(Stream xmlStream, CancellationToken token = default) { - using var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings + using XmlReader xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings { Async = true, IgnoreWhitespace = true, IgnoreComments = true }); - MarkupElement markupElement = null; - var stack = new Stack(); + MarkupElement? markupElement = null; + Stack stack = []; while (await xmlReader.ReadAsync()) { - var nodeType = xmlReader.NodeType; + XmlNodeType nodeType = xmlReader.NodeType; if (token.IsCancellationRequested) - { throw new TaskCanceledException("The operation was canceled."); - } // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (nodeType) { case XmlNodeType.Element: - var attributes = new Dictionary(); + Dictionary attributes = new(); if (xmlReader.HasAttributes) { for (int i = 0; i < xmlReader.AttributeCount; i++) @@ -93,7 +88,7 @@ public static async Task> DeserializeAsync(Stream xmlStream, xmlReader.MoveToElement(); } - var e = new MarkupElement + MarkupElement e = new() { Name = xmlReader.Name, Attributes = attributes, @@ -101,24 +96,16 @@ public static async Task> DeserializeAsync(Stream xmlStream, }; if (xmlReader.IsEmptyElement) - { stack.Peek().Children.Add(e); - } else - { stack.Push(e); - } break; case XmlNodeType.EndElement: - var element = stack.Pop(); + MarkupElement element = stack.Pop(); if (stack.Count == 0) - { markupElement = element; - } else - { stack.Peek().Children.Add(element); - } break; case XmlNodeType.Text: stack.Peek().Text = xmlReader.Value; @@ -126,7 +113,7 @@ public static async Task> DeserializeAsync(Stream xmlStream, } } - return markupElement.ToCards(); + return markupElement?.ToCards() ?? []; } #endregion @@ -142,7 +129,7 @@ public static async Task> DeserializeAsync(Stream xmlStream, /// enumerable public static IEnumerable Deserialize(FileInfo file) { - using var fs = file.OpenRead(); + using FileStream fs = file.OpenRead(); return Deserialize(fs); } @@ -155,7 +142,7 @@ public static IEnumerable Deserialize(FileInfo file) /// enumerable public static IEnumerable Deserialize(string xmlText) { - using var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlText)); + using MemoryStream xmlStream = new(Encoding.UTF8.GetBytes(xmlText)); return Deserialize(xmlStream); } @@ -168,25 +155,25 @@ public static IEnumerable Deserialize(string xmlText) /// enumerable public static IEnumerable Deserialize(Stream xmlStream) { - using var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings + using XmlReader xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings { Async = false, IgnoreWhitespace = true, IgnoreComments = true }); - MarkupElement markupElement = null; - var stack = new Stack(); + MarkupElement? markupElement = null; + Stack stack = []; while (xmlReader.Read()) { - var nodeType = xmlReader.NodeType; + XmlNodeType nodeType = xmlReader.NodeType; // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (nodeType) { case XmlNodeType.Element: - var attributes = new Dictionary(); + Dictionary attributes = new(); if (xmlReader.HasAttributes) { for (int i = 0; i < xmlReader.AttributeCount; i++) @@ -198,7 +185,7 @@ public static IEnumerable Deserialize(Stream xmlStream) xmlReader.MoveToElement(); } - var e = new MarkupElement + MarkupElement e = new() { Name = xmlReader.Name, Attributes = attributes, @@ -206,24 +193,16 @@ public static IEnumerable Deserialize(Stream xmlStream) }; if (xmlReader.IsEmptyElement) - { stack.Peek().Children.Add(e); - } else - { stack.Push(e); - } break; case XmlNodeType.EndElement: - var element = stack.Pop(); + MarkupElement element = stack.Pop(); if (stack.Count == 0) - { markupElement = element; - } else - { stack.Peek().Children.Add(element); - } break; case XmlNodeType.Text: stack.Peek().Text = xmlReader.Value; @@ -231,7 +210,7 @@ public static IEnumerable Deserialize(Stream xmlStream) } } - return markupElement.ToCards(); + return markupElement?.ToCards() ?? []; } #endregion @@ -246,11 +225,7 @@ public static IEnumerable Deserialize(Stream xmlStream) /// UTF-8 encoded XML file /// enumerable, will be null if return value is false /// True if deserialization is successful, otherwise false - public static bool TryDeserialize(FileInfo file, -#if NETSTANDARD2_1 || NET5_0_OR_GREATER - [NotNullWhen(true)] -#endif - out IEnumerable? cards) + public static bool TryDeserialize(FileInfo file, [NotNullWhen(true)] out IEnumerable? cards) { try { @@ -270,11 +245,7 @@ public static bool TryDeserialize(FileInfo file, /// UTF-8 encoded XML text /// enumerable, will be null if return value is false /// True if deserialization is successful, otherwise false - public static bool TryDeserialize(string xmlText, -#if NETSTANDARD2_1 || NET5_0_OR_GREATER - [NotNullWhen(true)] -#endif - out IEnumerable? cards) + public static bool TryDeserialize(string xmlText, [NotNullWhen(true)] out IEnumerable? cards) { try { @@ -294,11 +265,7 @@ public static bool TryDeserialize(string xmlText, /// UTF-8 encoded XML stream /// enumerable, will be null if return value is false /// True if deserialization is successful, otherwise false - public static bool TryDeserialize(Stream xmlStream, -#if NETSTANDARD2_1 || NET5_0_OR_GREATER - [NotNullWhen(true)] -#endif - out IEnumerable? cards) + public static bool TryDeserialize(Stream xmlStream, [NotNullWhen(true)] out IEnumerable? cards) { try { diff --git a/src/Kook.Net.CardMarkup/Extensions/CardBuilderExtensions.cs b/src/Kook.Net.CardMarkup/Extensions/CardBuilderExtensions.cs index 87247747..30c574d9 100644 --- a/src/Kook.Net.CardMarkup/Extensions/CardBuilderExtensions.cs +++ b/src/Kook.Net.CardMarkup/Extensions/CardBuilderExtensions.cs @@ -6,29 +6,28 @@ internal static class CardBuilderExtensions { public static IEnumerable ToCards(this MarkupElement element) { - var cards = new List(); + List cards = []; // Unwrap card-message element - var cardElements = element.Children; - foreach (var e in cardElements) + List cardElements = element.Children; + foreach (MarkupElement e in cardElements) { // Get Modules - var modules = e.Children[0].Children.Select(ToModule); + IEnumerable modules = e.Children[0].Children.Select(ToModule); // Create Card - var color = e.Attributes.GetColor(); - var theme = e.Attributes.GetCardTheme(); - var size = e.Attributes.GetCardSize(); + Color? color = e.Attributes.GetColor(); + CardTheme theme = e.Attributes.GetCardTheme(); + CardSize size = e.Attributes.GetCardSize(); - var builder = new CardBuilder() + CardBuilder builder = new CardBuilder() .WithTheme(theme) .WithSize(size); if (color.HasValue) - { builder.WithColor(color.Value); - } - builder.Modules.AddRange(modules); + foreach (IModuleBuilder module in modules) + builder.Modules.Add(module); cards.Add(builder.Build()); } @@ -60,26 +59,26 @@ private static IModuleBuilder ToModule(this MarkupElement element) private static HeaderModuleBuilder ToHeaderModule(this MarkupElement element) { - var text = element.Children[0].ToPlainTextElement(); + PlainTextElementBuilder text = element.Children[0].ToPlainTextElement(); return new HeaderModuleBuilder(text); } private static SectionModuleBuilder ToSectionModule(this MarkupElement element) { - var mode = element.Attributes.GetSectionAccessoryMode(); + SectionAccessoryMode? mode = element.Attributes.GetSectionAccessoryMode(); - var textXmlElement = element.Children.First(x => x.Name == "text"); - var accessoryXmlElement = element.Children.FirstOrDefault(x => x.Name == "accessory"); + MarkupElement textXmlElement = element.Children.First(x => x.Name == "text"); + MarkupElement? accessoryXmlElement = element.Children.Find(x => x.Name == "accessory"); - var text = textXmlElement.Children[0].ToElement(); - var accessory = accessoryXmlElement?.Children[0].ToElement(); + IElementBuilder text = textXmlElement.Children[0].ToElement(); + IElementBuilder? accessory = accessoryXmlElement?.Children[0].ToElement(); return new SectionModuleBuilder(text, mode, accessory); } private static ImageGroupModuleBuilder ToImagesModule(this MarkupElement element) { - var images = element.Children + List images = element.Children .Select(ToImageElement) .ToList(); @@ -88,7 +87,7 @@ private static ImageGroupModuleBuilder ToImagesModule(this MarkupElement element private static ContainerModuleBuilder ToContainerModule(this MarkupElement element) { - var images = element.Children + List images = element.Children .Select(ToImageElement) .ToList(); @@ -97,7 +96,7 @@ private static ContainerModuleBuilder ToContainerModule(this MarkupElement eleme private static ActionGroupModuleBuilder ToActionsModule(this MarkupElement element) { - var actions = element.Children + List actions = element.Children .Select(ToButtonElement) .ToList(); @@ -106,7 +105,7 @@ private static ActionGroupModuleBuilder ToActionsModule(this MarkupElement eleme private static ContextModuleBuilder ToContextModule(this MarkupElement element) { - var elements = element.Children + List elements = element.Children .Select(ToElement) .ToList(); @@ -148,9 +147,9 @@ private static CountdownModuleBuilder ToCountdownModule(this MarkupElement eleme { long? start = element.Attributes.GetLong("start", true); long end = element.Attributes.GetLong("end")!.Value; - var mode = element.Attributes.GetCountdownMode(); + CountdownMode mode = element.Attributes.GetCountdownMode(); - var endDt = DateTimeOffset.FromUnixTimeMilliseconds(end); + DateTimeOffset endDt = DateTimeOffset.FromUnixTimeMilliseconds(end); DateTimeOffset? startDt = start.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(start.Value) : null; return new CountdownModuleBuilder(mode, endDt, startDt); @@ -182,12 +181,12 @@ private static IElementBuilder ToElement(this MarkupElement element) private static PlainTextElementBuilder ToPlainTextElement(this MarkupElement element) { bool emoji = element.Attributes.GetBoolean("emoji", true); - return new PlainTextElementBuilder(element.Text.ParseText(), emoji); + return new PlainTextElementBuilder(element.Text?.ParseText(), emoji); } private static KMarkdownElementBuilder ToKMarkdownElement(this MarkupElement element) { - return new KMarkdownElementBuilder(element.Text.ParseText()); + return new KMarkdownElementBuilder(element.Text?.ParseText()); } private static ImageElementBuilder ToImageElement(this MarkupElement element) @@ -195,16 +194,16 @@ private static ImageElementBuilder ToImageElement(this MarkupElement element) string src = element.Attributes.GetString("src"); string alt = element.Attributes.GetString("alt", true); bool circle = element.Attributes.GetBoolean("circle", false); - var size = element.Attributes.GetImageSize(); + ImageSize size = element.Attributes.GetImageSize(); return new ImageElementBuilder(src, alt, size, circle); } private static ButtonElementBuilder ToButtonElement(this MarkupElement element) { - var textElement = element.Children[0].ToElement(); - var theme = element.Attributes.GetButtonTheme(); - var click = element.Attributes.GetButtonClickEventType(); + IElementBuilder textElement = element.Children[0].ToElement(); + ButtonTheme theme = element.Attributes.GetButtonTheme(); + ButtonClickEventType click = element.Attributes.GetButtonClickEventType(); string value = element.Attributes.GetString("value", true); return new ButtonElementBuilder { Text = textElement, Theme = theme, Click = click, Value = value }; @@ -217,7 +216,7 @@ private static ButtonElementBuilder ToButtonElement(this MarkupElement element) private static ParagraphStructBuilder ToParagraphStruct(this MarkupElement element) { int cols = element.Attributes.GetInt("cols", 1); - var elements = element.Children + List elements = element.Children .Select(ToElement) .ToList(); @@ -230,7 +229,7 @@ private static ParagraphStructBuilder ToParagraphStruct(this MarkupElement eleme private static string ParseText(this string text) { - var multiLine = text + IEnumerable multiLine = text .Split(["\r\n", "\r", "\n"], StringSplitOptions.None) .Where(x => !string.IsNullOrEmpty(x)) .Select(x => x.Trim()); diff --git a/src/Kook.Net.CardMarkup/Extensions/DictionaryExtensions.cs b/src/Kook.Net.CardMarkup/Extensions/DictionaryExtensions.cs index eba2ff92..0ef63023 100644 --- a/src/Kook.Net.CardMarkup/Extensions/DictionaryExtensions.cs +++ b/src/Kook.Net.CardMarkup/Extensions/DictionaryExtensions.cs @@ -2,13 +2,6 @@ namespace Kook.CardMarkup.Extensions; internal static class DictionaryExtensions { - public static TV GetValueOrDefault(this IDictionary dictionary, TK key, TV defaultValue = default) - { - if (dictionary.TryGetValue(key, out var value)) - { - return value; - } - - return defaultValue; - } + public static TV GetValueOrDefault(this IDictionary dictionary, TK key, TV defaultValue) => + dictionary.TryGetValue(key, out TV? value) ? value : defaultValue; } diff --git a/src/Kook.Net.CardMarkup/Extensions/ParserExtensions.cs b/src/Kook.Net.CardMarkup/Extensions/ParserExtensions.cs index 3a3e82e8..25fe1797 100644 --- a/src/Kook.Net.CardMarkup/Extensions/ParserExtensions.cs +++ b/src/Kook.Net.CardMarkup/Extensions/ParserExtensions.cs @@ -81,15 +81,14 @@ public static ButtonClickEventType GetButtonClickEventType(this Dictionary dictionary) + public static SectionAccessoryMode? GetSectionAccessoryMode(this Dictionary dictionary) { string theme = dictionary.GetValueOrDefault("mode", "unspecified"); return theme switch { - "unspecified" => SectionAccessoryMode.Unspecified, "left" => SectionAccessoryMode.Left, "right" => SectionAccessoryMode.Right, - _ => SectionAccessoryMode.Unspecified + _ => null }; } diff --git a/src/Kook.Net.CardMarkup/Models/MarkupElement.cs b/src/Kook.Net.CardMarkup/Models/MarkupElement.cs index 6142e55b..abc5ce6a 100644 --- a/src/Kook.Net.CardMarkup/Models/MarkupElement.cs +++ b/src/Kook.Net.CardMarkup/Models/MarkupElement.cs @@ -2,11 +2,11 @@ namespace Kook.CardMarkup.Models; internal record MarkupElement { - public string Name { get; set; } + public required string Name { get; set; } - public string Text { get; set; } + public string? Text { get; set; } - public Dictionary Attributes { get; set; } + public required Dictionary Attributes { get; set; } - public List Children { get; set; } + public required List Children { get; set; } } diff --git a/src/Kook.Net.Commands/Attributes/AliasAttribute.cs b/src/Kook.Net.Commands/Attributes/AliasAttribute.cs index 8bb0736a..564b2516 100644 --- a/src/Kook.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/AliasAttribute.cs @@ -20,7 +20,7 @@ namespace Kook.Commands; /// } /// /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AliasAttribute : Attribute { /// @@ -31,5 +31,8 @@ public class AliasAttribute : Attribute /// /// Creates a new with the given aliases. /// - public AliasAttribute(params string[] aliases) => Aliases = aliases; + public AliasAttribute(params string[] aliases) + { + Aliases = aliases; + } } diff --git a/src/Kook.Net.Commands/Attributes/CommandAttribute.cs b/src/Kook.Net.Commands/Attributes/CommandAttribute.cs index b309aa9e..739a4056 100644 --- a/src/Kook.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/CommandAttribute.cs @@ -3,13 +3,13 @@ namespace Kook.Commands; /// /// Marks the execution information for a command. /// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Method)] public class CommandAttribute : Attribute { /// /// Gets the text that has been set to be recognized as a command. /// - public string Text { get; } + public string? Text { get; } /// /// Specifies the of the command. This affects how the command is executed. @@ -27,7 +27,7 @@ public class CommandAttribute : Attribute /// /// overrides the value of this property if present. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Marks the aliases for a command. @@ -35,7 +35,7 @@ public class CommandAttribute : Attribute /// /// extends the base value of this if present. /// - public string[] Aliases { get; set; } + public string[]? Aliases { get; set; } /// /// Attaches remarks to your commands. @@ -43,16 +43,21 @@ public class CommandAttribute : Attribute /// /// overrides the value of this property if present. /// - public string Remarks { get; set; } + public string? Remarks { get; set; } /// - public CommandAttribute() => Text = null; + public CommandAttribute() + { + } /// /// Initializes a new attribute with the specified name. /// /// The name of the command. - public CommandAttribute(string text) => Text = text; + public CommandAttribute(string text) + { + Text = text; + } /// /// Initializes a new attribute with the specified name @@ -64,7 +69,7 @@ public class CommandAttribute : Attribute /// The aliases of the command. /// The remarks of the command. public CommandAttribute(string text, bool ignoreExtraArgs, - string summary = default, string[] aliases = default, string remarks = default) + string? summary = null, string[]? aliases = null, string? remarks = null) { Text = text; IgnoreExtraArgs = ignoreExtraArgs; diff --git a/src/Kook.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Kook.Net.Commands/Attributes/DontAutoLoadAttribute.cs index 9b21455e..cf689d2f 100644 --- a/src/Kook.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -8,7 +8,5 @@ namespace Kook.Commands; /// automatically (e.g. the method). If a non-public module marked /// with this attribute is attempted to be loaded manually, the loading process will also fail. /// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public class DontAutoLoadAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Class)] +public class DontAutoLoadAttribute : Attribute; diff --git a/src/Kook.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Kook.Net.Commands/Attributes/DontInjectAttribute.cs index 32cbe9e8..c742cc76 100644 --- a/src/Kook.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/DontInjectAttribute.cs @@ -22,7 +22,5 @@ namespace Kook.Commands; /// } /// /// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public class DontInjectAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Property)] +public class DontInjectAttribute : Attribute; diff --git a/src/Kook.Net.Commands/Attributes/GroupAttribute.cs b/src/Kook.Net.Commands/Attributes/GroupAttribute.cs index 170225ee..f6f29391 100644 --- a/src/Kook.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/GroupAttribute.cs @@ -3,20 +3,26 @@ namespace Kook.Commands; /// /// Marks the module as a command group. /// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Class)] public class GroupAttribute : Attribute { /// /// Gets the prefix set for the module. /// - public string Prefix { get; } + public string? Prefix { get; } /// - public GroupAttribute() => Prefix = null; + public GroupAttribute() + { + Prefix = null; + } /// /// Initializes a new with the provided prefix. /// /// The prefix of the module group. - public GroupAttribute(string prefix) => Prefix = prefix; + public GroupAttribute(string prefix) + { + Prefix = prefix; + } } diff --git a/src/Kook.Net.Commands/Attributes/NameAttribute.cs b/src/Kook.Net.Commands/Attributes/NameAttribute.cs index 1277a160..a5e289c8 100644 --- a/src/Kook.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/NameAttribute.cs @@ -4,7 +4,7 @@ namespace Kook.Commands; /// /// Marks the public name of a command, module, or parameter. /// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] public class NameAttribute : Attribute { /// @@ -16,5 +16,8 @@ public class NameAttribute : Attribute /// Marks the public name of a command, module, or parameter with the provided name. /// /// The public name of the object. - public NameAttribute(string text) => Text = text; + public NameAttribute(string text) + { + Text = text; + } } diff --git a/src/Kook.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/Kook.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs index 25dc87f9..c464b170 100644 --- a/src/Kook.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs @@ -4,7 +4,5 @@ namespace Kook.Commands; /// Instructs the command system to treat command parameters of this type /// as a collection of named arguments matching to its properties. /// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public sealed class NamedArgumentTypeAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Class)] +public sealed class NamedArgumentTypeAttribute : Attribute; diff --git a/src/Kook.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Kook.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 4a5cc008..35e1c0ee 100644 --- a/src/Kook.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -25,7 +25,7 @@ namespace Kook.Commands; /// => ReplyAsync(time); /// /// -[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] public sealed class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); @@ -42,7 +42,6 @@ public OverrideTypeReaderAttribute(Type overridenTypeReader) { if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}."); - TypeReader = overridenTypeReader; } } diff --git a/src/Kook.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Kook.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 54d38b97..5e387e7f 100644 --- a/src/Kook.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -4,7 +4,7 @@ namespace Kook.Commands; /// Requires the parameter to pass the specified precondition before execution can begin. /// /// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] public abstract class ParameterPreconditionAttribute : Attribute { /// @@ -14,6 +14,6 @@ public abstract class ParameterPreconditionAttribute : Attribute /// The parameter of the command being checked against. /// The raw value of the parameter. /// The service collection used for dependency injection. - public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, - IServiceProvider services); + public abstract Task CheckPermissionsAsync(ICommandContext context, + ParameterInfo parameter, object? value, IServiceProvider services); } diff --git a/src/Kook.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Kook.Net.Commands/Attributes/PreconditionAttribute.cs index e57fe0e9..367868b2 100644 --- a/src/Kook.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/PreconditionAttribute.cs @@ -4,7 +4,7 @@ namespace Kook.Commands; /// Requires the module or class to pass the specified precondition before execution can begin. /// /// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public abstract class PreconditionAttribute : Attribute { /// @@ -15,7 +15,7 @@ public abstract class PreconditionAttribute : Attribute /// be successful (A || B). Specifying = null or not at all will /// require *all* preconditions to pass, just like normal (A && B). /// - public string Group { get; set; } = null; + public string? Group { get; set; } /// /// When overridden in a derived class, uses the supplied string @@ -23,7 +23,7 @@ public abstract class PreconditionAttribute : Attribute /// Setting this for a class that doesn't override /// this property is a no-op. /// - public virtual string ErrorMessage + public virtual string? ErrorMessage { get => null; set { } diff --git a/src/Kook.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Kook.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index d9bc0062..07805af6 100644 --- a/src/Kook.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -3,7 +3,7 @@ namespace Kook.Commands; /// /// Requires the bot to have a specific permission in the channel a command is invoked in. /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class RequireBotPermissionAttribute : PreconditionAttribute { /// @@ -17,13 +17,13 @@ public class RequireBotPermissionAttribute : PreconditionAttribute public ChannelPermission? ChannelPermission { get; } /// - public override string ErrorMessage { get; set; } + public override string? ErrorMessage { get; set; } /// /// Gets or sets the error message if the precondition - /// fails due to being run outside of a Guild channel. + /// fails due to being run outside a Guild channel. /// - public string NotAGuildErrorMessage { get; set; } + public string? NotAGuildErrorMessage { get; set; } /// /// Requires the bot account to have a specific . @@ -57,25 +57,26 @@ public RequireBotPermissionAttribute(ChannelPermission permission) /// public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { - IGuildUser guildUser = null; - if (context.Guild != null) guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); + IGuildUser? guildUser = null; + if (context.Guild != null) + guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); if (GuildPermission.HasValue) { - if (guildUser == null) return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); - + if (guildUser == null) + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}."); } if (ChannelPermission.HasValue) { + if (guildUser == null) + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); ChannelPermissions perms; - if (context.Channel is IGuildChannel guildChannel) - perms = guildUser.GetPermissions(guildChannel); - else - perms = ChannelPermissions.All(context.Channel); - + perms = context.Channel is IGuildChannel guildChannel + ? guildUser.GetPermissions(guildChannel) + : ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}."); } diff --git a/src/Kook.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Kook.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index ea5119a1..9bc832f3 100644 --- a/src/Kook.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -20,7 +20,7 @@ public enum ContextType /// /// Requires the command to be invoked in a specified context (e.g. in guild, DM). /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class RequireContextAttribute : PreconditionAttribute { /// @@ -29,7 +29,7 @@ public class RequireContextAttribute : PreconditionAttribute public ContextType Contexts { get; } /// - public override string ErrorMessage { get; set; } + public override string? ErrorMessage { get; set; } /// Requires the command to be invoked in the specified context. /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. @@ -43,20 +43,23 @@ public class RequireContextAttribute : PreconditionAttribute /// } /// /// - public RequireContextAttribute(ContextType contexts) => Contexts = contexts; + public RequireContextAttribute(ContextType contexts) + { + Contexts = contexts; + } /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { bool isValid = false; + if ((Contexts & ContextType.Guild) != 0) + isValid = context.Channel is IGuildChannel; + if ((Contexts & ContextType.DM) != 0) + isValid = isValid || context.Channel is IDMChannel; - if ((Contexts & ContextType.Guild) != 0) isValid = context.Channel is IGuildChannel; - - if ((Contexts & ContextType.DM) != 0) isValid = isValid || context.Channel is IDMChannel; - - if (isValid) - return Task.FromResult(PreconditionResult.FromSuccess()); - else - return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}.")); + PreconditionResult preconditionResult = isValid + ? PreconditionResult.FromSuccess() + : PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}."); + return Task.FromResult(preconditionResult); } } diff --git a/src/Kook.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs b/src/Kook.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs index 80189560..3fbf660e 100644 --- a/src/Kook.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs @@ -11,26 +11,32 @@ namespace Kook.Commands; /// public class RequireRoleAttribute : PreconditionAttribute { - private readonly string _roleName; + private readonly string? _roleName; private readonly uint? _roleId; /// /// Gets or sets the error message if the precondition - /// fails due to being run outside of a Guild channel. + /// fails due to being run outside a Guild channel. /// - public string NotAGuildErrorMessage { get; set; } + public string? NotAGuildErrorMessage { get; set; } /// /// Requires that the user invoking the command to have a specific Role. /// /// Id of the role that the user must have. - public RequireRoleAttribute(uint roleId) => _roleId = roleId; + public RequireRoleAttribute(uint roleId) + { + _roleId = roleId; + } /// /// Requires that the user invoking the command to have a specific Role. /// /// Name of the role that the user must have. - public RequireRoleAttribute(string roleName) => _roleName = roleName; + public RequireRoleAttribute(string roleName) + { + _roleName = roleName; + } /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) @@ -42,12 +48,15 @@ public override Task CheckPermissionsAsync(ICommandContext c { return Task.FromResult(guildUser.RoleIds.Contains(_roleId.Value) ? PreconditionResult.FromSuccess() - : PreconditionResult.FromError(ErrorMessage ?? $"User requires guild role {context.Guild.GetRole(_roleId.Value).Name}.")); + : PreconditionResult.FromError(ErrorMessage ?? $"User requires guild role {guildUser.Guild.GetRole(_roleId.Value)?.Name}.")); } if (!string.IsNullOrEmpty(_roleName)) { - IEnumerable roleNames = guildUser.RoleIds.Select(x => guildUser.Guild.GetRole(x).Name); + IEnumerable roleNames = guildUser.RoleIds + .Select(x => guildUser.Guild.GetRole(x)) + .OfType() + .Select(x => x.Name); return Task.FromResult(roleNames.Contains(_roleName) ? PreconditionResult.FromSuccess() : PreconditionResult.FromError(ErrorMessage ?? $"User requires guild role {_roleName}.")); diff --git a/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserAttribute.cs b/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserAttribute.cs index 42eeef9f..c6f09089 100644 --- a/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserAttribute.cs @@ -25,7 +25,7 @@ namespace Kook.Commands; /// } /// /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class RequireUserAttribute : PreconditionAttribute { private readonly ulong _userId; @@ -34,10 +34,13 @@ public class RequireUserAttribute : PreconditionAttribute /// Initializes a new attribute with the specified user identifier. /// /// The identifier of the user. - public RequireUserAttribute(ulong userId) => _userId = userId; + public RequireUserAttribute(ulong userId) + { + _userId = userId; + } /// - public override string ErrorMessage { get; set; } + public override string? ErrorMessage { get; set; } /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) => diff --git a/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index f9e4f403..5719597e 100644 --- a/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -3,7 +3,7 @@ namespace Kook.Commands; /// /// Requires the user invoking the command to have a specified permission. /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class RequireUserPermissionAttribute : PreconditionAttribute { /// @@ -17,13 +17,13 @@ public class RequireUserPermissionAttribute : PreconditionAttribute public ChannelPermission? ChannelPermission { get; } /// - public override string ErrorMessage { get; set; } + public override string? ErrorMessage { get; set; } /// /// Gets or sets the error message if the precondition /// fails due to being run outside of a Guild channel. /// - public string NotAGuildErrorMessage { get; set; } + public string? NotAGuildErrorMessage { get; set; } /// /// Requires that the user invoking the command to have a specific . @@ -57,19 +57,20 @@ public RequireUserPermissionAttribute(ChannelPermission permission) /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { - IGuildUser guildUser = context.User as IGuildUser; + IGuildUser? guildUser = context.User as IGuildUser; if (GuildPermission.HasValue) { if (guildUser == null) return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); - if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}.")); } if (ChannelPermission.HasValue) { + if (guildUser == null) + return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); ChannelPermissions perms; if (context.Channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); diff --git a/src/Kook.Net.Commands/Attributes/PriorityAttribute.cs b/src/Kook.Net.Commands/Attributes/PriorityAttribute.cs index 1ced8abc..1080cba6 100644 --- a/src/Kook.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/PriorityAttribute.cs @@ -3,7 +3,7 @@ namespace Kook.Commands; /// /// Sets priority of commands. /// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Method)] public class PriorityAttribute : Attribute { /// @@ -14,5 +14,8 @@ public class PriorityAttribute : Attribute /// /// Initializes a new attribute with the given priority. /// - public PriorityAttribute(int priority) => Priority = priority; + public PriorityAttribute(int priority) + { + Priority = priority; + } } diff --git a/src/Kook.Net.Commands/Attributes/RemainderAttribute.cs b/src/Kook.Net.Commands/Attributes/RemainderAttribute.cs index 612aeac7..f9fe76a0 100644 --- a/src/Kook.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/RemainderAttribute.cs @@ -3,7 +3,5 @@ namespace Kook.Commands; /// /// Marks the input to not be parsed by the parser. /// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] -public class RemainderAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Parameter)] +public class RemainderAttribute : Attribute; diff --git a/src/Kook.Net.Commands/Attributes/RemarksAttribute.cs b/src/Kook.Net.Commands/Attributes/RemarksAttribute.cs index 197fc5be..15020f36 100644 --- a/src/Kook.Net.Commands/Attributes/RemarksAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/RemarksAttribute.cs @@ -4,7 +4,7 @@ namespace Kook.Commands; /// /// Attaches remarks to your commands. /// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class RemarksAttribute : Attribute { /// @@ -16,5 +16,8 @@ public class RemarksAttribute : Attribute /// Initializes a new attribute with the specified remarks. /// /// - public RemarksAttribute(string text) => Text = text; + public RemarksAttribute(string text) + { + Text = text; + } } diff --git a/src/Kook.Net.Commands/Attributes/SummaryAttribute.cs b/src/Kook.Net.Commands/Attributes/SummaryAttribute.cs index 8491c5f4..16eb1d4a 100644 --- a/src/Kook.Net.Commands/Attributes/SummaryAttribute.cs +++ b/src/Kook.Net.Commands/Attributes/SummaryAttribute.cs @@ -4,7 +4,7 @@ namespace Kook.Commands; /// /// Attaches a summary to your command. /// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] public class SummaryAttribute : Attribute { /// @@ -16,5 +16,8 @@ public class SummaryAttribute : Attribute /// Initializes a new attribute with the specified summary. /// /// - public SummaryAttribute(string text) => Text = text; + public SummaryAttribute(string text) + { + Text = text; + } } diff --git a/src/Kook.Net.Commands/Builders/CommandBuilder.cs b/src/Kook.Net.Commands/Builders/CommandBuilder.cs index e2092d81..5baadfd2 100644 --- a/src/Kook.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Kook.Net.Commands/Builders/CommandBuilder.cs @@ -20,27 +20,27 @@ public class CommandBuilder /// /// Gets or sets the callback that is invoked when this command is executed. /// - internal Func Callback { get; set; } + internal Func? Callback { get; set; } /// /// Gets or sets the name of this command. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the summary of this command. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Gets or sets the remarks of this command. /// - public string Remarks { get; set; } + public string? Remarks { get; set; } /// /// Gets or sets the primary alias of this command. /// - public string PrimaryAlias { get; set; } + public string? PrimaryAlias { get; set; } /// /// Gets or sets the run mode of this command. @@ -88,11 +88,10 @@ public class CommandBuilder internal CommandBuilder(ModuleBuilder module) { Module = module; - - _preconditions = new List(); - _parameters = new List(); - _attributes = new List(); - _aliases = new List(); + _preconditions = []; + _parameters = []; + _attributes = []; + _aliases = []; } #endregion @@ -105,7 +104,8 @@ internal CommandBuilder(ModuleBuilder module) /// The module builder that this command builder belongs to. /// The primary alias of this command. /// The callback that is invoked when this command is executed. - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string? primaryAlias, + Func callback) : this(module) { Kook.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); @@ -176,12 +176,13 @@ public CommandBuilder WithPriority(int priority) /// /// An array containing the aliases to add. /// This command builder. - public CommandBuilder AddAliases(params string[] aliases) + public CommandBuilder AddAliases(params string?[] aliases) { - for (int i = 0; i < aliases.Length; i++) + foreach (string? x in aliases) { - string alias = aliases[i] ?? ""; - if (!_aliases.Contains(alias)) _aliases.Add(alias); + string alias = x ?? string.Empty; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); } return this; @@ -266,17 +267,21 @@ internal CommandInfo Build(ModuleInfo info, CommandService service) if (_parameters.Count > 0) { - ParameterBuilder lastParam = _parameters[_parameters.Count - 1]; + ParameterBuilder lastParam = _parameters[^1]; - ParameterBuilder firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); + ParameterBuilder? firstMultipleParam = _parameters.Find(x => x.IsMultiple); if (firstMultipleParam != null && firstMultipleParam != lastParam) + { throw new InvalidOperationException( $"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); + } - ParameterBuilder firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); + ParameterBuilder? firstRemainderParam = _parameters.Find(x => x.IsRemainder); if (firstRemainderParam != null && firstRemainderParam != lastParam) + { throw new InvalidOperationException( $"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); + } } return new CommandInfo(this, info, service); diff --git a/src/Kook.Net.Commands/Builders/ModuleBuilder.cs b/src/Kook.Net.Commands/Builders/ModuleBuilder.cs index 0163b036..d7c5ec1b 100644 --- a/src/Kook.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Kook.Net.Commands/Builders/ModuleBuilder.cs @@ -9,7 +9,7 @@ public class ModuleBuilder { #region ModuleBuilder - private string _group; + private string? _group; private readonly List _commands; private readonly List _submodules; private readonly List _preconditions; @@ -24,32 +24,33 @@ public class ModuleBuilder /// /// Gets the parent module builder that this module builder belongs to. /// - public ModuleBuilder Parent { get; } + public ModuleBuilder? Parent { get; } /// /// Gets or sets the name of this module. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the summary of this module. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Gets or sets the remarks of this module. /// - public string Remarks { get; set; } + public string? Remarks { get; set; } /// /// Gets or sets the group of this module. /// - public string Group + public string? Group { get => _group; set { - _aliases.Remove(_group); + if (_group is not null) + _aliases.Remove(_group); _group = value; AddAliases(value); } @@ -78,9 +79,9 @@ public string Group /// /// Gets a read-only list of aliases that this module builder contains. /// - public IReadOnlyList Aliases => _aliases; + public IReadOnlyList Aliases => _aliases; - internal TypeInfo TypeInfo { get; set; } + internal TypeInfo? TypeInfo { get; set; } #endregion @@ -91,16 +92,16 @@ public string Group /// /// The command service that this module builder belongs to. /// The parent module builder that this module builder belongs to. - internal ModuleBuilder(CommandService service, ModuleBuilder parent) + internal ModuleBuilder(CommandService service, ModuleBuilder? parent) { Service = service; Parent = parent; - _commands = new List(); - _submodules = new List(); - _preconditions = new List(); - _attributes = new List(); - _aliases = new List(); + _commands = []; + _submodules = []; + _preconditions = []; + _attributes = []; + _aliases = []; } #endregion @@ -113,12 +114,11 @@ internal ModuleBuilder(CommandService service, ModuleBuilder parent) /// The command service that this module builder belongs to. /// The parent module builder that this module builder belongs to. /// The primary alias of this module. - internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) + internal ModuleBuilder(CommandService service, ModuleBuilder? parent, string primaryAlias) : this(service, parent) { Kook.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); - - _aliases = new List { primaryAlias }; + _aliases = [primaryAlias]; } /// @@ -159,12 +159,13 @@ public ModuleBuilder WithRemarks(string remarks) /// /// An array of aliases to add to this module. /// This module builder. - public ModuleBuilder AddAliases(params string[] aliases) + public ModuleBuilder AddAliases(params string?[] aliases) { - for (int i = 0; i < aliases.Length; i++) + foreach (string? x in aliases) { - string alias = aliases[i] ?? ""; - if (!_aliases.Contains(alias)) _aliases.Add(alias); + string alias = x ?? string.Empty; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); } return this; @@ -199,7 +200,8 @@ public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) /// The callback of this command. /// The function delegate that creates this command. /// This module builder. - public ModuleBuilder AddCommand(string primaryAlias, Func callback, + public ModuleBuilder AddCommand(string primaryAlias, + Func callback, Action createFunc) { CommandBuilder builder = new(this, primaryAlias, callback); @@ -255,12 +257,12 @@ internal ModuleBuilder AddModule(Action createFunc) /// The service provider that this module builder belongs to. /// The parent module that this module builder belongs to. /// The built module. - private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null) + private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo? parent = null) { //Default name to first alias - if (Name == null) Name = _aliases[0]; + Name ??= _aliases[0]; - if (TypeInfo != null && !TypeInfo.IsAbstract) + if (TypeInfo is { IsAbstract: false }) { IModuleBase moduleInstance = ReflectionUtils.CreateObject(TypeInfo, service, services); moduleInstance.OnModuleBuilding(service, this); diff --git a/src/Kook.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Kook.Net.Commands/Builders/ModuleClassBuilder.cs index d155a8d8..16e94617 100644 --- a/src/Kook.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Kook.Net.Commands/Builders/ModuleClassBuilder.cs @@ -9,49 +9,51 @@ internal static class ModuleClassBuilder public static async Task> SearchAsync(Assembly assembly, CommandService service) { - bool IsLoadableModule(TypeInfo info) => - info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) - && info.GetCustomAttribute() == null; - - List result = new(); + List result = []; foreach (TypeInfo typeInfo in assembly.DefinedTypes) { if (typeInfo.IsPublic || typeInfo.IsNestedPublic) { - if (IsValidModuleDefinition(typeInfo) && !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) result.Add(typeInfo); + if (IsValidModuleDefinition(typeInfo) && !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + result.Add(typeInfo); } else if (IsLoadableModule(typeInfo)) - await service._cmdLogger - .WarningAsync( - $"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.") + { + await service + ._cmdLogger + .WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.") .ConfigureAwait(false); + } } return result; + bool IsLoadableModule(TypeInfo info) => + info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) + && info.GetCustomAttribute() == null; } - public static Task> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => + public static Task> BuildAsync(CommandService service, + IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); - public static async Task> BuildAsync(IEnumerable validTypes, CommandService service, - IServiceProvider services) + public static async Task> BuildAsync(IEnumerable validTypes, + CommandService service, IServiceProvider services) { /*if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ - IEnumerable topLevelGroups = - validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); - - List builtTypes = new(); - - Dictionary result = new(); + IEnumerable topLevelGroups = validTypes + .Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); + List builtTypes = []; + Dictionary result = []; foreach (TypeInfo typeInfo in topLevelGroups) { // TODO: This shouldn't be the case; may be safe to remove? - if (result.ContainsKey(typeInfo.AsType())) continue; + if (result.ContainsKey(typeInfo.AsType())) + continue; ModuleBuilder module = new(service, null); @@ -62,7 +64,10 @@ public static async Task> BuildAsync(IEnumerable s { foreach (TypeInfo typeInfo in subTypes) { - if (!IsValidModuleDefinition(typeInfo)) continue; - - if (builtTypes.Contains(typeInfo)) continue; - + if (!IsValidModuleDefinition(typeInfo)) + continue; + if (builtTypes.Contains(typeInfo)) + continue; builder.AddModule((module) => { BuildModule(module, typeInfo, service, services); @@ -121,22 +126,22 @@ private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, Comman } //Check for unspecified info - if (builder.Aliases.Count == 0) builder.AddAliases(""); - - if (builder.Name == null) builder.Name = typeInfo.Name; + if (builder.Aliases.Count == 0) + builder.AddAliases(string.Empty); + builder.Name ??= typeInfo.Name; // Get all methods (including from inherited members), that are valid commands - IEnumerable validCommands = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + IEnumerable validCommands = typeInfo + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(IsValidCommandDefinition); foreach (MethodInfo method in validCommands) builder.AddCommand((command) => { BuildCommand(command, typeInfo, method, service, services); }); } - private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, - IServiceProvider serviceprovider) + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, + CommandService service, IServiceProvider serviceprovider) { IEnumerable attributes = method.GetCustomAttributes(); - foreach (Attribute attribute in attributes) { switch (attribute) @@ -144,8 +149,10 @@ private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, Meth case CommandAttribute command: builder.Summary ??= command.Summary; builder.Remarks ??= command.Remarks; - builder.AddAliases(command.Aliases ?? Array.Empty()); - builder.AddAliases(command.Text); + if (command.Aliases is { Length: > 0 }) + builder.AddAliases(command.Aliases); + if (!string.IsNullOrWhiteSpace(command.Text)) + builder.AddAliases(command.Text); builder.RunMode = command.RunMode; builder.Name ??= command.Text; builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs; @@ -174,16 +181,18 @@ private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, Meth } } - if (builder.Name == null) builder.Name = method.Name; - + builder.Name ??= method.Name; System.Reflection.ParameterInfo[] parameters = method.GetParameters(); - int pos = 0, count = parameters.Length; + int pos = 0; + int count = parameters.Length; foreach (System.Reflection.ParameterInfo paramInfo in parameters) - builder.AddParameter((parameter) => { BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); }); + builder.AddParameter(parameter => BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider)); Func createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + builder.Callback = ExecuteCallback; + return; - async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) + async Task ExecuteCallback(ICommandContext context, object?[] args, IServiceProvider services, CommandInfo cmd) { IModuleBase instance = createInstance(services); instance.SetContext(context); @@ -191,6 +200,7 @@ async Task ExecuteCallback(ICommandContext context, object[] args, ISer try { await instance.BeforeExecuteAsync(cmd).ConfigureAwait(false); + // ReSharper disable once MethodHasAsyncOverload instance.BeforeExecute(cmd); Task task = method.Invoke(instance, args) as Task ?? Task.Delay(0); @@ -205,22 +215,22 @@ async Task ExecuteCallback(ICommandContext context, object[] args, ISer finally { await instance.AfterExecuteAsync(cmd).ConfigureAwait(false); + // ReSharper disable once MethodHasAsyncOverload instance.AfterExecute(cmd); + // ReSharper disable once SuspiciousTypeConversion.Global (instance as IDisposable)?.Dispose(); } } - - builder.Callback = ExecuteCallback; } - private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, + private static void BuildParameter(ParameterBuilder builder, + System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) { IEnumerable attributes = paramInfo.GetCustomAttributes(); - Type paramType = paramInfo.ParameterType; - - builder.Name = paramInfo.Name; + Type? paramType = paramInfo.ParameterType; + builder.Name = paramInfo.Name ?? string.Empty; builder.IsOptional = paramInfo.IsOptional; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; @@ -234,21 +244,19 @@ private static void BuildParameter(ParameterBuilder builder, System.Reflection.P case OverrideTypeReaderAttribute typeReader: builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); break; - case ParamArrayAttribute _: + case ParamArrayAttribute: builder.IsMultiple = true; - paramType = paramType.GetElementType(); + paramType = paramType?.GetElementType(); break; - case ParameterPreconditionAttribute precon: - builder.AddPrecondition(precon); + case ParameterPreconditionAttribute precondition: + builder.AddPrecondition(precondition); break; case NameAttribute name: builder.Name = name.Text; break; - case RemainderAttribute _: + case RemainderAttribute: if (position != count - 1) - throw new InvalidOperationException( - $"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); - + throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType?.Name}.{paramInfo.Member.Name}"); builder.IsRemainder = true; break; default: @@ -258,24 +266,20 @@ private static void BuildParameter(ParameterBuilder builder, System.Reflection.P } builder.ParameterType = paramType; - - if (builder.TypeReader == null) - builder.TypeReader = service.GetDefaultTypeReader(paramType) - ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; + builder.TypeReader ??= service.GetDefaultTypeReader(paramType) + ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; } - internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) + internal static TypeReader GetTypeReader(CommandService service, Type? paramType, Type typeReaderType, IServiceProvider services) { - IDictionary readers = service.GetTypeReaders(paramType); - TypeReader reader = null; - if (readers != null) - if (readers.TryGetValue(typeReaderType, out reader)) - return reader; + IDictionary? readers = service.GetTypeReaders(paramType); + if (readers != null && readers.TryGetValue(typeReaderType, out TypeReader? reader)) + return reader; //We don't have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); - service.AddTypeReader(paramType, reader, false); - + if (paramType is not null) + service.AddTypeReader(paramType, reader, false); return reader; } diff --git a/src/Kook.Net.Commands/Builders/ParameterBuilder.cs b/src/Kook.Net.Commands/Builders/ParameterBuilder.cs index 3281114f..d5a148a3 100644 --- a/src/Kook.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Kook.Net.Commands/Builders/ParameterBuilder.cs @@ -25,12 +25,12 @@ public class ParameterBuilder /// /// Gets the type of this parameter. /// - public Type ParameterType { get; internal set; } + public Type? ParameterType { get; internal set; } /// /// Gets the type reader of this parameter. /// - public TypeReader TypeReader { get; set; } + public TypeReader? TypeReader { get; set; } /// /// Gets or sets a value that indicates whether this parameter is an optional parameter or not. @@ -50,12 +50,12 @@ public class ParameterBuilder /// /// Gets or sets the default value of this parameter. /// - public object DefaultValue { get; set; } + public object? DefaultValue { get; set; } /// /// Gets or sets the summary of this parameter. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Gets a read-only collection containing the preconditions of this parameter. @@ -77,9 +77,9 @@ public class ParameterBuilder /// The command builder that this parameter builder belongs to. internal ParameterBuilder(CommandBuilder command) { - _preconditions = new List(); - _attributes = new List(); - + _preconditions = []; + _attributes = []; + Name = string.Empty; Command = command; } @@ -97,7 +97,6 @@ internal ParameterBuilder(CommandBuilder command, string name, Type type) : this(command) { Kook.Preconditions.NotNull(name, nameof(name)); - Name = name; SetType(type); } @@ -109,11 +108,10 @@ internal ParameterBuilder(CommandBuilder command, string name, Type type) internal void SetType(Type type) { TypeReader = GetReader(type); - if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); - else if (type.IsArray) DefaultValue = Array.CreateInstance(type.GetElementType(), 0); - + else if (type.IsArray && type.GetElementType() is { } elementType) + DefaultValue = Array.CreateInstance(elementType, 0); ParameterType = type; } @@ -123,19 +121,20 @@ internal void SetType(Type type) /// The type of this parameter. /// The type reader of this parameter. /// The type for the command must be a class with a public parameterless constructor to use as a NamedArgumentType. - private TypeReader GetReader(Type type) + private TypeReader? GetReader(Type? type) { + if (type is null) return null; CommandService commands = Command.Module.Service; if (type.GetTypeInfo().GetCustomAttribute() != null) { IsRemainder = true; - TypeReader reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; + TypeReader? reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; if (reader == null) { Type readerType; try { - readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); + readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(type); } catch (ArgumentException ex) { @@ -144,19 +143,17 @@ private TypeReader GetReader(Type type) ex); } - reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); - commands.AddTypeReader(type, reader); + reader = (TypeReader?)Activator.CreateInstance(readerType, commands); + if (reader != null) + commands.AddTypeReader(type, reader); } return reader; } - IDictionary readers = commands.GetTypeReaders(type); - if (readers != null) - return readers.FirstOrDefault().Value; - else - return commands.GetDefaultTypeReader(type); + IDictionary? readers = commands.GetTypeReaders(type); + return readers != null ? readers.FirstOrDefault().Value : commands.GetDefaultTypeReader(type); } /// @@ -244,9 +241,8 @@ public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondit /// No type reader was found for this parameter, which must be specified. internal ParameterInfo Build(CommandInfo info) { - if ((TypeReader ??= GetReader(ParameterType)) == null) - throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified"); - + if ((TypeReader ??= GetReader(ParameterType)) is null) + throw new InvalidOperationException($"No type reader found for type {ParameterType?.Name}, one must be specified"); return new ParameterInfo(this, info, Command.Module.Service); } diff --git a/src/Kook.Net.Commands/CommandContext.cs b/src/Kook.Net.Commands/CommandContext.cs index 0e1fff57..65367507 100644 --- a/src/Kook.Net.Commands/CommandContext.cs +++ b/src/Kook.Net.Commands/CommandContext.cs @@ -7,7 +7,7 @@ public class CommandContext : ICommandContext public IKookClient Client { get; } /// - public IGuild Guild { get; } + public IGuild? Guild { get; } /// public IMessageChannel Channel { get; } diff --git a/src/Kook.Net.Commands/CommandException.cs b/src/Kook.Net.Commands/CommandException.cs index 118dc0f3..ec8c0ac8 100644 --- a/src/Kook.Net.Commands/CommandException.cs +++ b/src/Kook.Net.Commands/CommandException.cs @@ -19,7 +19,7 @@ public class CommandException : Exception /// The command information. /// The context of the command. /// The exception that interrupted the command execution. - public CommandException(CommandInfo command, ICommandContext context, Exception ex) + public CommandException(CommandInfo command, ICommandContext context, Exception? ex) : base($"Error occurred executing {command.GetLogText(context)}.", ex) { Command = command; diff --git a/src/Kook.Net.Commands/CommandMatch.cs b/src/Kook.Net.Commands/CommandMatch.cs index c78a88ac..73e2d4d7 100644 --- a/src/Kook.Net.Commands/CommandMatch.cs +++ b/src/Kook.Net.Commands/CommandMatch.cs @@ -28,8 +28,9 @@ public CommandMatch(CommandInfo command, string alias) /// The context of the command. /// The services to use. /// The result of the precondition check. - public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) - => Command.CheckPreconditionsAsync(context, services); + public Task CheckPreconditionsAsync(ICommandContext context, + IServiceProvider? services = null) => + Command.CheckPreconditionsAsync(context, services); /// /// Parses this command. @@ -39,9 +40,9 @@ public Task CheckPreconditionsAsync(ICommandContext context, /// The result of the precondition check. /// The services to use. /// The result of the parse. - public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, - IServiceProvider services = null) - => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); + public Task ParseAsync(ICommandContext context, SearchResult searchResult, + PreconditionResult? preconditionResult = null, IServiceProvider? services = null) => + Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); /// /// Executes this command. @@ -51,8 +52,9 @@ public Task ParseAsync(ICommandContext context, SearchResult search /// The parameters of the command. /// The services to use. /// The result of the execution. - public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) - => Command.ExecuteAsync(context, argList, paramList, services); + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, + IEnumerable paramList, IServiceProvider services) => + Command.ExecuteAsync(context, argList, paramList, services); /// /// Executes this command. @@ -61,6 +63,6 @@ public Task ExecuteAsync(ICommandContext context, IEnumerable a /// The result of the parse. /// The services to use. /// The result of the execution. - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) - => Command.ExecuteAsync(context, parseResult, services); + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) => + Command.ExecuteAsync(context, parseResult, services); } diff --git a/src/Kook.Net.Commands/CommandParser.cs b/src/Kook.Net.Commands/CommandParser.cs index 7ed97e89..16026335 100644 --- a/src/Kook.Net.Commands/CommandParser.cs +++ b/src/Kook.Net.Commands/CommandParser.cs @@ -15,7 +15,7 @@ private enum ParserPart public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary aliasMap) { - ParameterInfo curParam = null; + ParameterInfo? curParam = null; StringBuilder argBuilder = new(input.Length); int endPos = input.Length; ParserPart curPart = ParserPart.None; @@ -60,20 +60,19 @@ static char GetMatch(IReadOnlyDictionary dict, char ch) } //If this character is escaped, skip it - if (isEscaping) - if (curPos != endPos) - { - // if this character matches the quotation mark of the end of the string - // means that it should be escaped - // but if is not, then there is no reason to escape it then - if (c != matchQuote) - // if no reason to escape the next character, then re-add \ to the arg - argBuilder.Append('\\'); + if (isEscaping && curPos != endPos) + { + // if this character matches the quotation mark of the end of the string + // means that it should be escaped + // but if is not, then there is no reason to escape it then + if (c != matchQuote) + // if no reason to escape the next character, then re-add \ to the arg + argBuilder.Append('\\'); - argBuilder.Append(c); - isEscaping = false; - continue; - } + argBuilder.Append(c); + isEscaping = false; + continue; + } //Are we escaping the next character? if (c == '\\' && (curParam == null || !curParam.IsRemainder)) @@ -111,7 +110,7 @@ static char GetMatch(IReadOnlyDictionary dict, char ch) } //Has this parameter ended yet? - string argString = null; + string? argString = null; if (curPart == ParserPart.Parameter) { if (curPos == endPos || char.IsWhiteSpace(c)) @@ -139,8 +138,7 @@ static char GetMatch(IReadOnlyDictionary dict, char ch) { if (command.IgnoreExtraArgs) break; - else - return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); } TypeReaderResult typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); @@ -150,13 +148,11 @@ static char GetMatch(IReadOnlyDictionary dict, char ch) if (curParam.IsMultiple) { paramList.Add(typeReaderResult); - curPart = ParserPart.None; } else { argList.Add(typeReaderResult); - curParam = null; curPart = ParserPart.None; } @@ -165,26 +161,30 @@ static char GetMatch(IReadOnlyDictionary dict, char ch) } } - if (curParam != null && curParam.IsRemainder) + if (curParam is { IsRemainder: true }) { - TypeReaderResult typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false); - if (!typeReaderResult.IsSuccess) return ParseResult.FromError(typeReaderResult, curParam); + TypeReaderResult typeReaderResult = await curParam + .ParseAsync(context, argBuilder.ToString(), services) + .ConfigureAwait(false); + if (!typeReaderResult.IsSuccess) + return ParseResult.FromError(typeReaderResult, curParam); argList.Add(typeReaderResult); } - if (isEscaping) return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); - - if (curPart == ParserPart.QuotedParameter) return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete."); + if (isEscaping) + return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); + if (curPart == ParserPart.QuotedParameter) + return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete."); //Add missing optionals for (int i = argList.Count; i < command.Parameters.Count; i++) { ParameterInfo param = command.Parameters[i]; - if (param.IsMultiple) continue; - - if (!param.IsOptional) return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); - + if (param.IsMultiple) + continue; + if (!param.IsOptional) + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); } diff --git a/src/Kook.Net.Commands/CommandService.cs b/src/Kook.Net.Commands/CommandService.cs index 17acd980..9320bac9 100644 --- a/src/Kook.Net.Commands/CommandService.cs +++ b/src/Kook.Net.Commands/CommandService.cs @@ -44,13 +44,13 @@ public event Func Log /// This event is fired when a command has been executed, successfully or not. When a command fails to /// execute during parsing or precondition stage, the CommandInfo may not be returned. /// - public event Func CommandExecuted + public event Func CommandExecuted { add => _commandExecutedEvent.Add(value); remove => _commandExecutedEvent.Remove(value); } - internal readonly AsyncEvent> _commandExecutedEvent = new(); + internal readonly AsyncEvent> _commandExecutedEvent = new(); private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; @@ -82,8 +82,9 @@ public event Func CommandExecuted /// /// Represents all loaded within . /// - public ILookup TypeReaders => - _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); + public ILookup TypeReaders => _typeReaders + .SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })) + .ToLookup(x => x.Key, x => x.Value); /// /// Initializes a new class. @@ -106,7 +107,7 @@ public CommandService(CommandServiceConfig config) _ignoreExtraArgs = config.IgnoreExtraArgs; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; - _quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary()).ToImmutableDictionary(); + _quotationMarkAliasMap = config.QuotationMarkAliasMap.ToImmutableDictionary(); if (_defaultRunMode == RunMode.Default) throw new InvalidOperationException("The default run mode cannot be set to Default."); @@ -123,7 +124,8 @@ public CommandService(CommandServiceConfig config) _defaultTypeReaders = new ConcurrentDictionary(); foreach (Type type in PrimitiveParsers.SupportedTypes) { - _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + if (PrimitiveTypeReader.Create(type) is { } typeReader) + _defaultTypeReaders[type] = typeReader; _defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]); } @@ -163,9 +165,7 @@ public async Task CreateModuleAsync(string primaryAlias, Action CreateModuleAsync(string primaryAlias, Action AddModuleAsync(Type type, IServiceProvider services) { services ??= EmptyServiceProvider.Instance; - await _moduleLock.WaitAsync().ConfigureAwait(false); try { @@ -219,13 +218,13 @@ public async Task AddModuleAsync(Type type, IServiceProvider service if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException("This module has already been added."); - KeyValuePair module = - (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); + Dictionary moduleInfos = await ModuleClassBuilder + .BuildAsync(this, services, typeInfo) + .ConfigureAwait(false); + KeyValuePair module = moduleInfos.FirstOrDefault(); - if (module.Value == default(ModuleInfo)) - throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); - - _typedModuleDefs[module.Key] = module.Value; + _typedModuleDefs[module.Key] = module.Value + ?? throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); return LoadModuleInternal(module.Value); } @@ -271,11 +270,10 @@ public async Task> AddModulesAsync(Assembly assembly, IS private ModuleInfo LoadModuleInternal(ModuleInfo module) { _moduleDefs.Add(module); - - foreach (CommandInfo command in module.Commands) _map.AddCommand(command); - - foreach (ModuleInfo submodule in module.Submodules) LoadModuleInternal(submodule); - + foreach (CommandInfo command in module.Commands) + _map.AddCommand(command); + foreach (ModuleInfo submodule in module.Submodules) + LoadModuleInternal(submodule); return module; } @@ -323,8 +321,8 @@ public async Task RemoveModuleAsync(Type type) await _moduleLock.WaitAsync().ConfigureAwait(false); try { - if (!_typedModuleDefs.TryRemove(type, out ModuleInfo module)) return false; - + if (!_typedModuleDefs.TryRemove(type, out ModuleInfo? module)) + return false; return RemoveModuleInternal(module); } finally @@ -336,11 +334,10 @@ public async Task RemoveModuleAsync(Type type) private bool RemoveModuleInternal(ModuleInfo module) { if (!_moduleDefs.Remove(module)) return false; - - foreach (CommandInfo cmd in module.Commands) _map.RemoveCommand(cmd); - - foreach (ModuleInfo submodule in module.Submodules) RemoveModuleInternal(submodule); - + foreach (CommandInfo cmd in module.Commands) + _map.RemoveCommand(cmd); + foreach (ModuleInfo submodule in module.Submodules) + RemoveModuleInternal(submodule); return true; } @@ -358,8 +355,7 @@ private bool RemoveModuleInternal(ModuleInfo module) /// /// The object type to be read by the . /// An instance of the to be added. - public void AddTypeReader(TypeReader reader) - => AddTypeReader(typeof(T), reader); + public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); /// /// Adds a custom to this for the supplied object @@ -374,8 +370,11 @@ public void AddTypeReader(TypeReader reader) public void AddTypeReader(Type type, TypeReader reader) { if (_defaultTypeReaders.ContainsKey(type)) - _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + { + _ = _cmdLogger.WarningAsync( + $"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}. " + "To suppress this message, use AddTypeReader(reader, true)."); + } AddTypeReader(type, reader, true); } @@ -392,8 +391,8 @@ public void AddTypeReader(Type type, TypeReader reader) /// Defines whether the should replace the default one for /// if it exists. /// - public void AddTypeReader(TypeReader reader, bool replaceDefault) - => AddTypeReader(typeof(T), reader, replaceDefault); + public void AddTypeReader(TypeReader reader, bool replaceDefault) => + AddTypeReader(typeof(T), reader, replaceDefault); /// /// Adds a custom to this for the supplied object @@ -411,19 +410,18 @@ public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) { if (replaceDefault && HasDefaultTypeReader(type)) { - _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); + _defaultTypeReaders.AddOrUpdate(type, reader, (_, _) => reader); if (type.GetTypeInfo().IsValueType) { Type nullableType = typeof(Nullable<>).MakeGenericType(type); TypeReader nullableReader = NullableTypeReader.Create(type, reader); - _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); + _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (_, _) => nullableReader); } } else { - ConcurrentDictionary readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); + ConcurrentDictionary readers = _typeReaders.GetOrAdd(type, _ => new ConcurrentDictionary()); readers[reader.GetType()] = reader; - if (type.GetTypeInfo().IsValueType) AddNullableTypeReader(type, reader); } } @@ -445,49 +443,50 @@ public bool TryRemoveTypeReader(Type type, bool isDefaultTypeReader, out IDictio if (isDefaultTypeReader) { - bool isSuccess = _defaultTypeReaders.TryRemove(type, out TypeReader result); - if (isSuccess) readers.Add(result?.GetType(), result); - - return isSuccess; + if (!_defaultTypeReaders.TryRemove(type, out TypeReader? result)) + return false; + readers.Add(result.GetType(), result); + return true; } else { - bool isSuccess = _typeReaders.TryRemove(type, out ConcurrentDictionary result); - - if (isSuccess) readers = result; - - return isSuccess; + if (!_typeReaders.TryRemove(type, out ConcurrentDictionary? result)) + return false; + readers = result; + return true; } } internal bool HasDefaultTypeReader(Type type) { - if (_defaultTypeReaders.ContainsKey(type)) return true; - + if (_defaultTypeReaders.ContainsKey(type)) + return true; TypeInfo typeInfo = type.GetTypeInfo(); - if (typeInfo.IsEnum) return true; - - return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.EntityType)); + if (typeInfo.IsEnum) + return true; + return _entityTypeReaders.Exists(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.EntityType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { - ConcurrentDictionary readers = - _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); + ConcurrentDictionary readers = _typeReaders + .GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), _ => new ConcurrentDictionary()); TypeReader nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); readers[nullableReader.GetType()] = nullableReader; } - internal IDictionary GetTypeReaders(Type type) + internal IDictionary? GetTypeReaders(Type? type) { - if (_typeReaders.TryGetValue(type, out ConcurrentDictionary definedTypeReaders)) return definedTypeReaders; - - return null; + if (type == null) return null; + if (!_typeReaders.TryGetValue(type, out ConcurrentDictionary? definedTypeReaders)) return null; + return definedTypeReaders; } - internal TypeReader GetDefaultTypeReader(Type type) + internal TypeReader? GetDefaultTypeReader(Type? type) { - if (_defaultTypeReaders.TryGetValue(type, out TypeReader reader)) return reader; + if (type == null) return null; + if (_defaultTypeReaders.TryGetValue(type, out TypeReader? reader)) + return reader; TypeInfo typeInfo = type.GetTypeInfo(); @@ -499,8 +498,8 @@ internal TypeReader GetDefaultTypeReader(Type type) return reader; } - Type underlyingType = Nullable.GetUnderlyingType(type); - if (underlyingType != null && underlyingType.IsEnum) + Type? underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType is { IsEnum: true }) { reader = NullableTypeReader.Create(underlyingType, EnumTypeReader.GetReader(underlyingType)); _defaultTypeReaders[type] = reader; @@ -509,12 +508,16 @@ internal TypeReader GetDefaultTypeReader(Type type) //Is this an entity? for (int i = 0; i < _entityTypeReaders.Count; i++) - if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) + { + if (type == _entityTypeReaders[i].EntityType + || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) { reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; - _defaultTypeReaders[type] = reader; + if (reader is not null) + _defaultTypeReaders[type] = reader; return reader; } + } return null; } @@ -529,8 +532,8 @@ internal TypeReader GetDefaultTypeReader(Type type) /// The context of the command. /// The position of which the command starts at. /// The result containing the matching commands. - public SearchResult Search(ICommandContext context, int argPos) - => Search(context.Message.Content.Substring(argPos)); + public SearchResult Search(ICommandContext context, int argPos) => + Search(context.Message.Content.Substring(argPos)); /// /// Searches for the command. @@ -538,8 +541,7 @@ public SearchResult Search(ICommandContext context, int argPos) /// The context of the command. /// The command string. /// The result containing the matching commands. - public SearchResult Search(ICommandContext context, string input) - => Search(input); + public SearchResult Search(ICommandContext context, string input) => Search(input); /// /// Searches for the command. @@ -549,12 +551,11 @@ public SearchResult Search(ICommandContext context, string input) public SearchResult Search(string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); - ImmutableArray matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); + ImmutableArray matches = [.._map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority)]; - if (matches.Length > 0) - return SearchResult.FromSuccess(input, matches); - else - return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); + return matches.Length > 0 + ? SearchResult.FromSuccess(input, matches) + : SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } /// @@ -569,8 +570,8 @@ public SearchResult Search(string input) /// command execution. /// public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, - MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); + MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => + ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); /// /// Executes the command. @@ -587,45 +588,42 @@ public async Task ExecuteAsync(ICommandContext context, string input, I MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services ??= EmptyServiceProvider.Instance; - SearchResult searchResult = Search(input); - IResult validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); - if (validationResult is SearchResult result) { await _commandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } - - if (validationResult is MatchResult matchResult) return await HandleCommandPipeline(matchResult, context, services); - + if (validationResult is MatchResult matchResult) + return await HandleCommandPipeline(matchResult, context, services); return validationResult; } private async Task HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) { - if (!matchResult.IsSuccess) return matchResult; + if (!matchResult.IsSuccess) + return matchResult; if (matchResult.Pipeline is ParseResult parseResult) { - if (!parseResult.IsSuccess) + if (!parseResult.IsSuccess || !matchResult.Match.HasValue) { - await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, parseResult); + await _commandExecutedEvent.InvokeAsync(matchResult.Match?.Command, context, parseResult); return parseResult; } IResult executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services); if (!executeResult.IsSuccess - && !(executeResult is RuntimeResult - || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + && executeResult is not (RuntimeResult or ExecuteResult)) + // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult); return executeResult; } - if (matchResult.Pipeline is PreconditionResult preconditionResult) + if (matchResult is { Pipeline: PreconditionResult preconditionResult, Match: not null }) { await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); return preconditionResult; @@ -637,7 +635,8 @@ private async Task HandleCommandPipeline(MatchResult matchResult, IComm // Calculates the 'score' of a command given a parse result private float CalculateScore(CommandMatch match, ParseResult parseResult) { - float argValuesScore = 0, paramValuesScore = 0; + float argValuesScore = 0; + float paramValuesScore = 0; if (match.Command.Parameters.Count > 0) { diff --git a/src/Kook.Net.Commands/CommandServiceConfig.cs b/src/Kook.Net.Commands/CommandServiceConfig.cs index 59b16631..2e0c171c 100644 --- a/src/Kook.Net.Commands/CommandServiceConfig.cs +++ b/src/Kook.Net.Commands/CommandServiceConfig.cs @@ -34,7 +34,7 @@ public class CommandServiceConfig /// /// Collection of aliases for matching pairs of string delimiters. /// The dictionary stores the opening delimiter as a key, and the matching closing delimiter as the value. - /// If no value is supplied will be used, which contains + /// If no value is supplied will be used, which contains /// many regional equivalents. /// Only values that are specified in this map will be used as string delimiters, so if " is removed then /// it won't be used. @@ -50,7 +50,7 @@ public class CommandServiceConfig /// } /// /// - public Dictionary QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; + public Dictionary QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.DefaultAliasMap; /// /// Gets or sets a value that indicates whether extra parameters should be ignored. diff --git a/src/Kook.Net.Commands/EmptyServiceProvider.cs b/src/Kook.Net.Commands/EmptyServiceProvider.cs index 443048c0..f532b191 100644 --- a/src/Kook.Net.Commands/EmptyServiceProvider.cs +++ b/src/Kook.Net.Commands/EmptyServiceProvider.cs @@ -4,5 +4,5 @@ internal class EmptyServiceProvider : IServiceProvider { public static readonly EmptyServiceProvider Instance = new(); - public object GetService(Type serviceType) => null; + public object? GetService(Type serviceType) => null; } diff --git a/src/Kook.Net.Commands/Extensions/CommandServiceExtensions.cs b/src/Kook.Net.Commands/Extensions/CommandServiceExtensions.cs index dca7a209..40bc0132 100644 --- a/src/Kook.Net.Commands/Extensions/CommandServiceExtensions.cs +++ b/src/Kook.Net.Commands/Extensions/CommandServiceExtensions.cs @@ -17,8 +17,7 @@ public static class CommandServiceExtensions public static async Task> GetExecutableCommandsAsync(this ICollection commands, ICommandContext context, IServiceProvider provider) { - List executableCommands = new(); - + List executableCommands = []; var tasks = commands.Select(async c => { PreconditionResult result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); @@ -26,11 +25,10 @@ public static async Task> GetExecutableCommands }); var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - foreach (var result in results) - if (result.PreconditionResult.IsSuccess) - executableCommands.Add(result.Command); - + IEnumerable successes = results + .Where(r => r.PreconditionResult.IsSuccess) + .Select(r => r.Command); + executableCommands.AddRange(successes); return executableCommands; } @@ -43,9 +41,9 @@ public static async Task> GetExecutableCommands /// /// A read-only collection of commands that can be executed under the current context. /// - public static Task> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, - IServiceProvider provider) - => GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); + public static Task> GetExecutableCommandsAsync(this CommandService commandService, + ICommandContext context, IServiceProvider provider) => + GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); /// /// Returns commands that can be executed under the current context. @@ -59,16 +57,14 @@ public static Task> GetExecutableCommandsAsync( public static async Task> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider) { - List executableCommands = new(); + List executableCommands = []; executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); - - IEnumerable>> tasks = - module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + IEnumerable>> tasks = module + .Submodules + .Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); IReadOnlyCollection[] results = await Task.WhenAll(tasks).ConfigureAwait(false); - executableCommands.AddRange(results.SelectMany(c => c)); - return executableCommands; } } diff --git a/src/Kook.Net.Commands/Extensions/MessageExtensions.cs b/src/Kook.Net.Commands/Extensions/MessageExtensions.cs index 4261b0fc..68b23ed5 100644 --- a/src/Kook.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Kook.Net.Commands/Extensions/MessageExtensions.cs @@ -29,7 +29,8 @@ public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) /// /// Gets whether the message starts with the provided string. /// - public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) + public static bool HasStringPrefix(this IUserMessage msg, string str, + ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { string text = msg.Content; if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType)) @@ -49,15 +50,19 @@ public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int a if (msg.Type == MessageType.Text) { string text = msg.Content; - if (string.IsNullOrEmpty(text) || text.Length <= 6 || text[0] != '@') return false; + if (string.IsNullOrEmpty(text) || text.Length <= 6 || text[0] != '@') + return false; int endPos = text.IndexOf('#'); - if (endPos == -1) return false; + if (endPos == -1) + return false; endPos += 4; - if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; + if (text.Length < endPos + 2 || text[endPos + 1] != ' ') + return false; - if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out ulong userId, TagMode.PlainText)) return false; + if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out ulong userId, TagMode.PlainText)) + return false; if (userId == user.Id) { @@ -68,19 +73,19 @@ public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int a else if (msg.Type == MessageType.KMarkdown) { string text = msg.Content; -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (string.IsNullOrEmpty(text) || text.Length <= 10 || text[..5] != "(met)") return false; -#else - if (string.IsNullOrEmpty(text) || text.Length <= 10 || text.Substring(0, 5) != "(met)") return false; -#endif + if (string.IsNullOrEmpty(text) || text.Length <= 10 || text[..5] != "(met)") + return false; int endPos = text.IndexOf("(met)", 5, StringComparison.Ordinal); - if (endPos == -1) return false; + if (endPos == -1) + return false; endPos += 4; - if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; + if (text.Length < endPos + 2 || text[endPos + 1] != ' ') + return false; - if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out ulong userId, TagMode.KMarkdown)) return false; + if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out ulong userId, TagMode.KMarkdown)) + return false; if (userId == user.Id) { diff --git a/src/Kook.Net.Commands/IModuleBase.cs b/src/Kook.Net.Commands/IModuleBase.cs index e6c54b76..a2628d22 100644 --- a/src/Kook.Net.Commands/IModuleBase.cs +++ b/src/Kook.Net.Commands/IModuleBase.cs @@ -26,7 +26,7 @@ public interface IModuleBase void BeforeExecute(CommandInfo command); /// - /// Executed after a command is ran in this module base. + /// Executed after a command is run in this module base. /// /// The command that ran. void AfterExecute(CommandInfo command); diff --git a/src/Kook.Net.Commands/Info/CommandInfo.cs b/src/Kook.Net.Commands/Info/CommandInfo.cs index d880d146..d3bfcff2 100644 --- a/src/Kook.Net.Commands/Info/CommandInfo.cs +++ b/src/Kook.Net.Commands/Info/CommandInfo.cs @@ -18,12 +18,13 @@ namespace Kook.Commands; public class CommandInfo { private static readonly MethodInfo _convertParamsMethod = - typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); + typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)) + ?? throw new MissingMethodException(nameof(CommandInfo), nameof(ConvertParamsList)); - private static readonly ConcurrentDictionary, object>> _arrayConverters = new(); + private static readonly ConcurrentDictionary, object?>> _arrayConverters = new(); private readonly CommandService _commandService; - private readonly Func _action; + private readonly Func? _action; /// /// Gets the module that the command belongs in. @@ -42,7 +43,7 @@ public class CommandInfo /// This field returns the summary of the command. and can be /// useful in help commands and various implementation that fetches details of the command for the user. /// - public string Summary { get; } + public string? Summary { get; } /// /// Gets the remarks of the command. @@ -51,7 +52,7 @@ public class CommandInfo /// This field returns the summary of the command. and can be /// useful in help commands and various implementation that fetches details of the command for the user. /// - public string Remarks { get; } + public string? Remarks { get; } /// /// Gets the priority of the command. This is used when there are multiple overloads of the command. @@ -98,7 +99,7 @@ internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService s { Module = module; - Name = builder.Name; + Name = builder.Name ?? string.Empty; Summary = builder.Summary; Remarks = builder.Remarks; @@ -108,12 +109,9 @@ internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService s Aliases = module.Aliases .Permutate(builder.Aliases, (first, second) => { - if (first == "") - return second; - else if (second == "") - return first; - else - return first + service._separatorChar + second; + if (first == string.Empty) return second; + if (second == string.Empty) return first; + return first + service._separatorChar + second; }) .Select(x => service._caseSensitive ? x : x.ToLowerInvariant()) .ToImmutableArray(); @@ -122,7 +120,7 @@ internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService s Attributes = builder.Attributes.ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); - HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[builder.Parameters.Count - 1].IsMultiple; + HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[^1].IsMultiple; IgnoreExtraArgs = builder.IgnoreExtraArgs; _action = builder.Callback; @@ -135,41 +133,44 @@ internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService s /// The context of the command. /// The services to be used for precondition checking. /// A that indicates whether the precondition check was successful. - public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) + public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider? services = null) { services ??= EmptyServiceProvider.Instance; + PreconditionResult moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false); + if (!moduleResult.IsSuccess) + return moduleResult; + + PreconditionResult commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false); + if (!commandResult.IsSuccess) + return commandResult; + + return PreconditionResult.FromSuccess(); + async Task CheckGroups(IEnumerable preconditions, string type) { - foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) + foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) { if (preconditionGroup.Key == null) + { foreach (PreconditionAttribute precondition in preconditionGroup) { PreconditionResult result = await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } + } else { - List results = new(); + List results = []; foreach (PreconditionAttribute precondition in preconditionGroup) results.Add(await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false)); - - if (!results.Any(p => p.IsSuccess)) + if (!results.Exists(p => p.IsSuccess)) return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); } } return PreconditionGroupResult.FromSuccess(); } - - PreconditionResult moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false); - if (!moduleResult.IsSuccess) return moduleResult; - - PreconditionResult commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false); - if (!commandResult.IsSuccess) return commandResult; - - return PreconditionResult.FromSuccess(); } /// @@ -182,16 +183,16 @@ async Task CheckGroups(IEnumerable pr /// The services to be used for parsing. /// A that indicates whether the parsing was successful. public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, - PreconditionResult preconditionResult = null, IServiceProvider services = null) + PreconditionResult? preconditionResult = null, IServiceProvider? services = null) { services ??= EmptyServiceProvider.Instance; - if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); - - if (preconditionResult != null && !preconditionResult.IsSuccess) return ParseResult.FromError(preconditionResult); - - string input = searchResult.Text.Substring(startIndex); + if (!searchResult.IsSuccess) + return ParseResult.FromError(searchResult); + if (preconditionResult is { IsSuccess: false }) + return ParseResult.FromError(preconditionResult); + string input = searchResult.Text?[startIndex..] ?? string.Empty; return await CommandParser .ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap) .ConfigureAwait(false); @@ -206,21 +207,22 @@ public async Task ParseAsync(ICommandContext context, int startInde /// An that indicates whether the execution was successful. public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { - if (!parseResult.IsSuccess) return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); + if (!parseResult.IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); - object[] argList = new object[parseResult.ArgValues.Count]; + object?[] argList = new object[parseResult.ArgValues.Count]; for (int i = 0; i < parseResult.ArgValues.Count; i++) { - if (!parseResult.ArgValues[i].IsSuccess) return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); - + if (!parseResult.ArgValues[i].IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); argList[i] = parseResult.ArgValues[i].Values.First().Value; } - object[] paramList = new object[parseResult.ParamValues.Count]; + object?[] paramList = new object[parseResult.ParamValues.Count]; for (int i = 0; i < parseResult.ParamValues.Count; i++) { - if (!parseResult.ParamValues[i].IsSuccess) return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); - + if (!parseResult.ParamValues[i].IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); paramList[i] = parseResult.ParamValues[i].Values.First().Value; } @@ -235,19 +237,19 @@ public Task ExecuteAsync(ICommandContext context, ParseResult parseResu /// The parameters of the command. /// The services to be used for execution. /// An that indicates whether the execution was successful. - public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, - IServiceProvider services) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, + IEnumerable paramList, IServiceProvider services) { services ??= EmptyServiceProvider.Instance; try { - object[] args = GenerateArgs(argList, paramList); + object?[] args = GenerateArgs(argList, paramList); for (int position = 0; position < Parameters.Count; position++) { ParameterInfo parameter = Parameters[position]; - object argument = args[position]; + object? argument = args[position]; PreconditionResult result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); if (!result.IsSuccess) { @@ -261,7 +263,7 @@ public async Task ExecuteAsync(ICommandContext context, IEnumerable { await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); }); + _ = Task.Run(async () => await ExecuteInternalAsync(context, args, services).ConfigureAwait(false)); break; } @@ -273,17 +275,18 @@ public async Task ExecuteAsync(ICommandContext context, IEnumerable ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteInternalAsync(ICommandContext context, object?[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try { - Task task = _action(context, args, services, this); + Task? task = _action?.Invoke(context, args, services, this); if (task is Task resultTask) { IResult result = await resultTask.ConfigureAwait(false); await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); - if (result is RuntimeResult execResult) return execResult; + if (result is RuntimeResult execResult) + return execResult; } else if (task is Task execTask) { @@ -293,7 +296,8 @@ private async Task ExecuteInternalAsync(ICommandContext context, object } else { - await task.ConfigureAwait(false); + if (task != null) + await task.ConfigureAwait(false); ExecuteResult result = ExecuteResult.FromSuccess(); await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); } @@ -303,22 +307,22 @@ private async Task ExecuteInternalAsync(ICommandContext context, object } catch (Exception ex) { - Exception originalEx = ex; - while (ex is TargetInvocationException) //Happens with void-returning commands - ex = ex.InnerException; + Exception? internalEx = ex; + while (internalEx is TargetInvocationException) //Happens with void-returning commands + internalEx = internalEx.InnerException; - CommandException wrappedEx = new(this, context, ex); + CommandException wrappedEx = new(this, context, internalEx); await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); - ExecuteResult result = ExecuteResult.FromError(ex); + ExecuteResult result = ExecuteResult.FromError(internalEx); await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); if (Module.Service._throwOnError) { - if (ex == originalEx) + if (internalEx == ex) throw; else - ExceptionDispatchInfo.Capture(ex).Throw(); + ExceptionDispatchInfo.Capture(internalEx ?? ex).Throw(); } return result; @@ -329,28 +333,30 @@ private async Task ExecuteInternalAsync(ICommandContext context, object } } - private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) + private object?[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) { int argCount = Parameters.Count; - object[] array = new object[Parameters.Count]; - if (HasVarArgs) argCount--; + object?[] array = new object?[Parameters.Count]; + if (HasVarArgs) + argCount--; int i = 0; - foreach (object arg in argList) + foreach (object? arg in argList) { - if (i == argCount) throw new InvalidOperationException("Command was invoked with too many parameters."); - + if (i == argCount) + throw new InvalidOperationException("Command was invoked with too many parameters."); array[i++] = arg; } - if (i < argCount) throw new InvalidOperationException("Command was invoked with too few parameters."); + if (i < argCount) + throw new InvalidOperationException("Command was invoked with too few parameters."); - if (HasVarArgs) + if (HasVarArgs && Parameters[^1].Type is { } argType) { - Func, object> func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t => + Func, object?> func = _arrayConverters.GetOrAdd(argType, t => { MethodInfo method = _convertParamsMethod.MakeGenericMethod(t); - return (Func, object>)method.CreateDelegate(typeof(Func, object>)); + return (Func, object?>)method.CreateDelegate(typeof(Func, object?>)); }); array[i] = func(paramsList); } @@ -358,14 +364,11 @@ private object[] GenerateArgs(IEnumerable argList, IEnumerable p return array; } - private static T[] ConvertParamsList(IEnumerable paramsList) - => paramsList.Cast().ToArray(); + private static T[] ConvertParamsList(IEnumerable paramsList) => + paramsList.Cast().ToArray(); - internal string GetLogText(ICommandContext context) - { - if (context.Guild != null) - return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; - else - return $"\"{Name}\" for {context.User} in {context.Channel}"; - } + internal string GetLogText(ICommandContext context) => + context.Guild != null + ? $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}" + : $"\"{Name}\" for {context.User} in {context.Channel}"; } diff --git a/src/Kook.Net.Commands/Info/ModuleInfo.cs b/src/Kook.Net.Commands/Info/ModuleInfo.cs index 8c6c9e20..028052ca 100644 --- a/src/Kook.Net.Commands/Info/ModuleInfo.cs +++ b/src/Kook.Net.Commands/Info/ModuleInfo.cs @@ -16,22 +16,22 @@ public class ModuleInfo /// /// Gets the name of this module. /// - public string Name { get; } + public string? Name { get; } /// /// Gets the summary of this module. /// - public string Summary { get; } + public string? Summary { get; } /// /// Gets the remarks of this module. /// - public string Remarks { get; } + public string? Remarks { get; } /// /// Gets the group name (main prefix) of this module. /// - public string Group { get; } + public string? Group { get; } /// /// Gets a read-only list of aliases associated with this module. @@ -61,14 +61,14 @@ public class ModuleInfo /// /// Gets the parent module of this submodule if applicable. /// - public ModuleInfo Parent { get; } + public ModuleInfo? Parent { get; } /// /// Gets a value that indicates whether this module is a submodule or not. /// public bool IsSubmodule => Parent != null; - internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) + internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo? parent = null) { Service = service; @@ -78,21 +78,21 @@ internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvi Group = builder.Group; Parent = parent; - Aliases = BuildAliases(builder, service).ToImmutableArray(); - Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); - Preconditions = BuildPreconditions(builder).ToImmutableArray(); - Attributes = BuildAttributes(builder).ToImmutableArray(); - - Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); + Aliases = [..BuildAliases(builder, service)]; + Commands = [..builder.Commands.Select(x => x.Build(this, service))]; + Preconditions = [..BuildPreconditions(builder)]; + Attributes = [..BuildAttributes(builder)]; + Submodules = [..BuildSubmodules(builder, service, services)]; } private static IEnumerable BuildAliases(ModuleBuilder builder, CommandService service) { - List result = builder.Aliases.ToList(); + List result = builder.Aliases.ToList(); Queue builderQueue = new(); - ModuleBuilder parent = builder; - while ((parent = parent.Parent) != null) builderQueue.Enqueue(parent); + ModuleBuilder? parent = builder; + while ((parent = parent.Parent) != null) + builderQueue.Enqueue(parent); while (builderQueue.Count > 0) { @@ -100,32 +100,23 @@ private static IEnumerable BuildAliases(ModuleBuilder builder, CommandSe // permute in reverse because we want to *prefix* our aliases result = level.Aliases.Permutate(result, (first, second) => { - if (first == "") - return second; - else if (second == "") - return first; - else - return first + service._separatorChar + second; + if (first == string.Empty) return second; + if (second == string.Empty) return first; + return first + service._separatorChar + second; }).ToList(); } - return result; + return result.OfType().Distinct(); } - private List BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) - { - List result = new(); - - foreach (ModuleBuilder submodule in parent.Modules) result.Add(submodule.Build(service, services, this)); - - return result; - } + private List BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) => + parent.Modules.Select(submodule => submodule.Build(service, services, this)).ToList(); private static List BuildPreconditions(ModuleBuilder builder) { - List result = new(); + List result = []; - ModuleBuilder parent = builder; + ModuleBuilder? parent = builder; while (parent != null) { result.AddRange(parent.Preconditions); @@ -137,9 +128,9 @@ private static List BuildPreconditions(ModuleBuilder buil private static List BuildAttributes(ModuleBuilder builder) { - List result = new(); + List result = []; - ModuleBuilder parent = builder; + ModuleBuilder? parent = builder; while (parent != null) { result.AddRange(parent.Attributes); diff --git a/src/Kook.Net.Commands/Info/ParameterInfo.cs b/src/Kook.Net.Commands/Info/ParameterInfo.cs index 0237861a..7bf0f89c 100644 --- a/src/Kook.Net.Commands/Info/ParameterInfo.cs +++ b/src/Kook.Net.Commands/Info/ParameterInfo.cs @@ -7,7 +7,7 @@ namespace Kook.Commands; /// /// Provides the information of a parameter. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class ParameterInfo { private readonly TypeReader _reader; @@ -25,7 +25,7 @@ public class ParameterInfo /// /// Gets the summary of this parameter. /// - public string Summary { get; } + public string? Summary { get; } /// /// Gets a value that indicates whether this parameter is optional or not. @@ -45,12 +45,12 @@ public class ParameterInfo /// /// Gets the type of the parameter. /// - public Type Type { get; } + public Type? Type { get; } /// /// Gets the default value for this optional parameter if applicable. /// - public object DefaultValue { get; } + public object? DefaultValue { get; } /// /// Gets a read-only list of precondition that apply to this parameter. @@ -78,7 +78,7 @@ internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandSer Preconditions = builder.Preconditions.ToImmutableArray(); Attributes = builder.Attributes.ToImmutableArray(); - _reader = builder.TypeReader; + _reader = builder.TypeReader!; // TypeReader is always set before building } /// @@ -88,16 +88,17 @@ internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandSer /// The argument that is being parsed. /// The service provider that is used to resolve services. /// A that indicates whether the precondition is successful or not. - public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) + public async Task CheckPreconditionsAsync(ICommandContext context, + object? arg, IServiceProvider? services = null) { services ??= EmptyServiceProvider.Instance; - foreach (ParameterPreconditionAttribute precondition in Preconditions) { - PreconditionResult result = await precondition.CheckPermissionsAsync(context, this, arg, services).ConfigureAwait(false); + PreconditionResult result = await precondition + .CheckPermissionsAsync(context, this, arg, services) + .ConfigureAwait(false); if (!result.IsSuccess) return result; } - return PreconditionResult.FromSuccess(); } @@ -108,7 +109,8 @@ public async Task CheckPreconditionsAsync(ICommandContext co /// The input string. /// The service provider that is used to resolve services. /// A that contains the parsing result. - public async Task ParseAsync(ICommandContext context, string input, IServiceProvider services = null) + public async Task ParseAsync(ICommandContext context, + string input, IServiceProvider? services = null) { services ??= EmptyServiceProvider.Instance; return await _reader.ReadAsync(context, input, services).ConfigureAwait(false); diff --git a/src/Kook.Net.Commands/Map/CommandMap.cs b/src/Kook.Net.Commands/Map/CommandMap.cs index f31cb554..99d38f54 100644 --- a/src/Kook.Net.Commands/Map/CommandMap.cs +++ b/src/Kook.Net.Commands/Map/CommandMap.cs @@ -4,7 +4,7 @@ internal class CommandMap { private readonly CommandService _service; private readonly CommandMapNode _root; - private static readonly string[] BlankAliases = { "" }; + private static readonly string[] BlankAliases = [""]; public CommandMap(CommandService service) { @@ -14,13 +14,16 @@ public CommandMap(CommandService service) public void AddCommand(CommandInfo command) { - foreach (string text in command.Aliases) _root.AddCommand(_service, text, 0, command); + foreach (string text in command.Aliases) + _root.AddCommand(_service, text, 0, command); } public void RemoveCommand(CommandInfo command) { - foreach (string text in command.Aliases) _root.RemoveCommand(_service, text, 0, command); + foreach (string text in command.Aliases) + _root.RemoveCommand(_service, text, 0, command); } - public IEnumerable GetCommands(string text) => _root.GetCommands(_service, text, 0, text != ""); + public IEnumerable GetCommands(string text) => + _root.GetCommands(_service, text, 0, text != ""); } diff --git a/src/Kook.Net.Commands/Map/CommandMapNode.cs b/src/Kook.Net.Commands/Map/CommandMapNode.cs index c1f6c89e..99bd28f4 100644 --- a/src/Kook.Net.Commands/Map/CommandMapNode.cs +++ b/src/Kook.Net.Commands/Map/CommandMapNode.cs @@ -5,46 +5,47 @@ namespace Kook.Commands; internal class CommandMapNode { - private static readonly char[] WhitespaceChars = { ' ', '\r', '\n' }; + private static readonly char[] WhitespaceChars = [' ', '\r', '\n']; private readonly ConcurrentDictionary _nodes; private readonly string _name; private readonly object _lockObj = new(); private ImmutableArray _commands; + // ReSharper disable InconsistentlySynchronizedField public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; + // ReSharper restore InconsistentlySynchronizedField public CommandMapNode(string name) { _name = name; - _nodes = new ConcurrentDictionary(); - _commands = ImmutableArray.Create(); + _nodes = []; + _commands = []; } /// Cannot add commands to the root node. public void AddCommand(CommandService service, string text, int index, CommandInfo command) { int nextSegment = NextSegment(text, index, service._separatorChar); - string name; lock (_lockObj) { - if (text == "") + if (string.IsNullOrEmpty(text)) { - if (_name == "") throw new InvalidOperationException("Cannot add commands to the root node."); - + if (string.IsNullOrEmpty(_name)) + throw new InvalidOperationException("Cannot add commands to the root node."); _commands = _commands.Add(command); } else { - if (nextSegment == -1) - name = text.Substring(index); - else - name = text.Substring(index, nextSegment - index); - - string fullName = _name == "" ? name : _name + service._separatorChar + name; + string name = nextSegment == -1 + ? text[index..] + : text.Substring(index, nextSegment - index); + string fullName = _name == string.Empty + ? name + : _name + service._separatorChar + name; CommandMapNode nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName)); - nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); + nextNode.AddCommand(service, nextSegment == -1 ? string.Empty : text, nextSegment + 1, command); } } } @@ -55,17 +56,14 @@ public void RemoveCommand(CommandService service, string text, int index, Comman lock (_lockObj) { - if (text == "") + if (string.IsNullOrEmpty(text)) _commands = _commands.Remove(command); else { - string name; - if (nextSegment == -1) - name = text.Substring(index); - else - name = text.Substring(index, nextSegment - index); - - if (_nodes.TryGetValue(name, out CommandMapNode nextNode)) + string name = nextSegment == -1 + ? text[index..] + : text.Substring(index, nextSegment - index); + if (_nodes.TryGetValue(name, out CommandMapNode? nextNode)) { nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); if (nextNode.IsEmpty) _nodes.TryRemove(name, out nextNode); @@ -81,19 +79,18 @@ public IEnumerable GetCommands(CommandService service, string text if (visitChildren) { - string name; - CommandMapNode nextNode; - //Search for next segment int nextSegment = NextSegment(text, index, service._separatorChar); - if (nextSegment == -1) - name = text.Substring(index); - else - name = text.Substring(index, nextSegment - index); + string name = nextSegment == -1 + ? text[index..] + : text.Substring(index, nextSegment - index); - if (_nodes.TryGetValue(name, out nextNode)) - foreach (CommandMatch cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true)) + if (_nodes.TryGetValue(name, out CommandMapNode? nextNode)) + { + IEnumerable matches = nextNode.GetCommands(service, nextSegment == -1 ? string.Empty : text, nextSegment + 1, true); + foreach (CommandMatch cmd in matches) yield return cmd; + } //Check if this is the last command segment before args nextSegment = NextSegment(text, index, WhitespaceChars, service._separatorChar); @@ -101,8 +98,10 @@ public IEnumerable GetCommands(CommandService service, string text { name = text.Substring(index, nextSegment - index); if (_nodes.TryGetValue(name, out nextNode)) - foreach (CommandMatch cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false)) + { + foreach (CommandMatch cmd in nextNode.GetCommands(service, nextSegment == -1 ? string.Empty : text, nextSegment + 1, false)) yield return cmd; + } } } } @@ -112,12 +111,12 @@ public IEnumerable GetCommands(CommandService service, string text private static int NextSegment(string text, int startIndex, char[] separators, char except) { int lowest = int.MaxValue; - for (int i = 0; i < separators.Length; i++) - if (separators[i] != except) - { - int index = text.IndexOf(separators[i], startIndex); - if (index != -1 && index < lowest) lowest = index; - } + foreach (char x in separators.Where(x => x != except)) + { + int index = text.IndexOf(x, startIndex); + if (index != -1 && index < lowest) + lowest = index; + } return lowest != int.MaxValue ? lowest : -1; } diff --git a/src/Kook.Net.Commands/ModuleBase.cs b/src/Kook.Net.Commands/ModuleBase.cs index ecfc5768..ac0c27f1 100644 --- a/src/Kook.Net.Commands/ModuleBase.cs +++ b/src/Kook.Net.Commands/ModuleBase.cs @@ -5,9 +5,7 @@ namespace Kook.Commands; /// /// Provides a base class for a command module to inherit from. /// -public abstract class ModuleBase : ModuleBase -{ -} +public abstract class ModuleBase : ModuleBase; /// /// Provides a base class for a command module to inherit from. @@ -23,7 +21,7 @@ public abstract class ModuleBase : IModuleBase /// /// /// - public T Context { get; private set; } + public T Context { get; private set; } = null!; // Set by SetContext /// /// Sends a file to the source channel. @@ -31,7 +29,7 @@ public abstract class ModuleBase : IModuleBase /// /// The file path of the file. /// - /// + /// /// The name of the file. /// /// The type of the attachment. @@ -42,10 +40,13 @@ public abstract class ModuleBase : IModuleBase /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// /// The request options for this async request. - protected virtual async Task> ReplyFileAsync(string path, string fileName = null, - AttachmentType type = AttachmentType.File, bool isQuote = true, bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendFileAsync(path, fileName, type, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + protected virtual async Task> ReplyFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, bool isQuote = true, bool isEphemeral = false, + RequestOptions? options = null) => + await Context.Channel.SendFileAsync(path, filename, type, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// Sends a file to the source channel. @@ -53,7 +54,7 @@ protected virtual async Task> ReplyFileAsync(strin /// /// Stream of the file to be sent. /// - /// + /// /// The name of the file. /// /// The type of the attachment. @@ -64,10 +65,13 @@ protected virtual async Task> ReplyFileAsync(strin /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// /// The request options for this async request. - protected virtual async Task> ReplyFileAsync(Stream stream, string fileName = null, - AttachmentType type = AttachmentType.File, bool isQuote = true, bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendFileAsync(stream, fileName, type, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + protected virtual async Task> ReplyFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, bool isQuote = true, bool isEphemeral = false, + RequestOptions? options = null) => + await Context.Channel.SendFileAsync(stream, filename, type, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// Sends a file to the source channel. @@ -80,10 +84,12 @@ protected virtual async Task> ReplyFileAsync(Strea /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// /// The request options for this async request. - protected virtual async Task> ReplyFileAsync(FileAttachment attachment, bool isQuote = true, - bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendFileAsync(attachment, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + protected virtual async Task> ReplyFileAsync(FileAttachment attachment, + bool isQuote = true, bool isEphemeral = false, RequestOptions? options = null) => + await Context.Channel.SendFileAsync(attachment, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// Sends a text message to the source channel. @@ -99,9 +105,11 @@ protected virtual async Task> ReplyFileAsync(FileA /// /// The request options for this async request. protected virtual async Task> ReplyTextAsync(string message, bool isQuote = true, - bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendTextAsync(message, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + bool isEphemeral = false, RequestOptions? options = null) => + await Context.Channel.SendTextAsync(message, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// Sends a card message to the source channel. @@ -116,10 +124,12 @@ protected virtual async Task> ReplyTextAsync(strin /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// /// The request options for this async request. - protected virtual async Task> ReplyCardsAsync(IEnumerable cards, bool isQuote = true, - bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendCardsAsync(cards, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + protected virtual async Task> ReplyCardsAsync(IEnumerable cards, + bool isQuote = true, bool isEphemeral = false, RequestOptions? options = null) => + await Context.Channel.SendCardsAsync(cards, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// Sends a card message to the source channel. @@ -135,10 +145,11 @@ protected virtual async Task> ReplyCardsAsync(IEnu /// /// The request options for this async request. protected virtual async Task> ReplyCardAsync(ICard card, - bool isQuote = true, - bool isEphemeral = false, RequestOptions options = null) => - await Context.Channel.SendCardAsync(card, isQuote ? new Quote(Context.Message.Id) : null, - isEphemeral ? Context.User : null, options).ConfigureAwait(false); + bool isQuote = true, bool isEphemeral = false, RequestOptions? options = null) => + await Context.Channel.SendCardAsync(card, + isQuote ? new MessageReference(Context.Message.Id) : null, + isEphemeral ? Context.User : null, options) + .ConfigureAwait(false); /// /// The method to execute asynchronously before executing the command. @@ -183,7 +194,7 @@ protected virtual void OnModuleBuilding(CommandService commandService, ModuleBui void IModuleBase.SetContext(ICommandContext context) { - T newValue = context as T; + T? newValue = context as T; Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}."); } @@ -191,7 +202,9 @@ void IModuleBase.SetContext(ICommandContext context) void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); Task IModuleBase.AfterExecuteAsync(CommandInfo command) => AfterExecuteAsync(command); void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); - void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); + + void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => + OnModuleBuilding(commandService, builder); #endregion } diff --git a/src/Kook.Net.Commands/Readers/ChannelTypeReader.cs b/src/Kook.Net.Commands/Readers/ChannelTypeReader.cs index 3817ecbf..df5f2808 100644 --- a/src/Kook.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/ChannelTypeReader.cs @@ -40,14 +40,16 @@ public override async Task ReadAsync(ICommandContext context, foreach (IGuildChannel channel in channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase))) AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f); - if (results.Count > 0) return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()); + if (results.Count > 0) + return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()); } return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); } - private void AddResult(Dictionary results, T channel, float score) + private void AddResult(Dictionary results, T? channel, float score) { - if (channel != null && !results.ContainsKey(channel.Id)) results.Add(channel.Id, new TypeReaderValue(channel, score)); + if (channel != null && !results.ContainsKey(channel.Id)) + results.Add(channel.Id, new TypeReaderValue(channel, score)); } } diff --git a/src/Kook.Net.Commands/Readers/EnumTypeReader.cs b/src/Kook.Net.Commands/Readers/EnumTypeReader.cs index 612561f4..d52e0aa3 100644 --- a/src/Kook.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/EnumTypeReader.cs @@ -7,13 +7,15 @@ internal static class EnumTypeReader { public static TypeReader GetReader(Type type) { + if (!type.IsEnum) throw new ArgumentException("Type must be an enum.", nameof(type)); Type baseType = Enum.GetUnderlyingType(type); ConstructorInfo constructor = typeof(EnumTypeReader<>).MakeGenericType(baseType).GetTypeInfo().DeclaredConstructors.First(); - return (TypeReader)constructor.Invoke(new object[] { type, PrimitiveParsers.Get(baseType) }); + return (TypeReader) constructor.Invoke([type, PrimitiveParsers.Get(baseType)]); } } internal class EnumTypeReader : TypeReader + where T : struct, Enum { private readonly IReadOnlyDictionary _enumsByName; private readonly IReadOnlyDictionary _enumsByValue; @@ -42,21 +44,17 @@ public EnumTypeReader(Type type, TryParseDelegate parser) /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - object enumValue; + object? enumValue; if (_tryParse(input, out T baseValue)) { - if (_enumsByValue.TryGetValue(baseValue, out enumValue)) - return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); - else - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); - } - else - { - if (_enumsByName.TryGetValue(input.ToLower(), out enumValue)) - return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); - else - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); + return Task.FromResult(_enumsByValue.TryGetValue(baseValue, out enumValue) + ? TypeReaderResult.FromSuccess(enumValue) + : TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); } + + return Task.FromResult(_enumsByName.TryGetValue(input.ToLower(), out enumValue) + ? TypeReaderResult.FromSuccess(enumValue) + : TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); } } diff --git a/src/Kook.Net.Commands/Readers/MessageTypeReader.cs b/src/Kook.Net.Commands/Readers/MessageTypeReader.cs index 98e6db4b..b5a76870 100644 --- a/src/Kook.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/MessageTypeReader.cs @@ -11,10 +11,9 @@ public class MessageTypeReader : TypeReader public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { //By Id (1.0) - if (Guid.TryParse(input, out Guid id)) - if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) - return TypeReaderResult.FromSuccess(msg); - + if (Guid.TryParse(input, out Guid id) + && await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) + return TypeReaderResult.FromSuccess(msg); return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found."); } } diff --git a/src/Kook.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/Kook.Net.Commands/Readers/NamedArgumentTypeReader.cs index 6c11290f..7e96d10c 100644 --- a/src/Kook.Net.Commands/Readers/NamedArgumentTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/NamedArgumentTypeReader.cs @@ -7,13 +7,17 @@ namespace Kook.Commands; internal sealed class NamedArgumentTypeReader : TypeReader where T : class, new() { - private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo().DeclaredProperties + private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo() + .DeclaredProperties .Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) .ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); private readonly CommandService _commands; - public NamedArgumentTypeReader(CommandService commands) => _commands = commands; + public NamedArgumentTypeReader(CommandService commands) + { + _commands = commands; + } public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { @@ -22,26 +26,30 @@ public override async Task ReadAsync(ICommandContext context, int beginRead = 0, currentRead = 0; while (state != ReadState.End) + { try { - PropertyInfo prop = Read(out string arg); - object propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); - if (propVal != null) - prop.SetMethod.Invoke(result, new[] { propVal }); + PropertyInfo? prop = Read(out string arg); + object? propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); + if (prop?.SetMethod is not null && propVal != null) + prop.SetMethod.Invoke(result, [propVal]); else + { return TypeReaderResult.FromError(CommandError.ParseFailed, - $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); + $"Could not parse the argument for the parameter '{prop?.Name}' as type '{prop?.PropertyType}'."); + } } catch (Exception ex) { return TypeReaderResult.FromError(ex); } + } return TypeReaderResult.FromSuccess(result); - PropertyInfo Read(out string arg) + PropertyInfo? Read(out string arg) { - string currentParam = null; + string? currentParam = null; char match = '\0'; for (; currentRead < input.Length; currentRead++) @@ -52,53 +60,40 @@ PropertyInfo Read(out string arg) case ReadState.LookingForParameter: if (char.IsWhiteSpace(currentChar)) continue; - else - { - beginRead = currentRead; - state = ReadState.InParameter; - } - + beginRead = currentRead; + state = ReadState.InParameter; break; case ReadState.InParameter: if (currentChar != ':') continue; - else - { - currentParam = input.Substring(beginRead, currentRead - beginRead); - state = ReadState.LookingForArgument; - } - + currentParam = input.Substring(beginRead, currentRead - beginRead); + state = ReadState.LookingForArgument; break; case ReadState.LookingForArgument: if (char.IsWhiteSpace(currentChar)) continue; - else - { - beginRead = currentRead; - state = QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match) - ? ReadState.InQuotedArgument - : ReadState.InArgument; - } - + beginRead = currentRead; + state = QuotationAliasUtils.DefaultAliasMap.TryGetValue(currentChar, out match) + ? ReadState.InQuotedArgument + : ReadState.InArgument; break; case ReadState.InArgument: if (!char.IsWhiteSpace(currentChar)) continue; - else - return GetPropAndValue(out arg); + return GetPropAndValue(out arg); case ReadState.InQuotedArgument: if (currentChar != match) continue; - else - return GetPropAndValue(out arg); + return GetPropAndValue(out arg); } } - if (currentParam == null) throw new InvalidOperationException("No parameter name was read."); + if (currentParam == null) + throw new InvalidOperationException("No parameter name was read."); return GetPropAndValue(out arg); - PropertyInfo GetPropAndValue(out string argv) + PropertyInfo? GetPropAndValue(out string argv) { bool quoted = state == ReadState.InQuotedArgument; state = currentRead == (quoted ? input.Length - 1 : input.Length) @@ -113,12 +108,14 @@ PropertyInfo GetPropAndValue(out string argv) else argv = input.Substring(beginRead, currentRead - beginRead); + if (currentParam == null) return null; return _tProps[currentParam]; } } - async Task ReadArgumentAsync(PropertyInfo prop, string arg) + async Task ReadArgumentAsync(PropertyInfo? prop, string arg) { + if (prop is null) return null; Type elemType = prop.PropertyType; bool isCollection = false; if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) @@ -127,44 +124,44 @@ async Task ReadArgumentAsync(PropertyInfo prop, string arg) isCollection = true; } - OverrideTypeReaderAttribute overridden = prop.GetCustomAttribute(); - TypeReader reader = overridden != null + OverrideTypeReaderAttribute? overridden = prop.GetCustomAttribute(); + TypeReader? reader = overridden != null ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) : _commands.GetDefaultTypeReader(elemType) - ?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value; + ?? _commands.GetTypeReaders(elemType)?.FirstOrDefault().Value; if (reader != null) { if (isCollection) { MethodInfo method = _readMultipleMethod.MakeGenericMethod(elemType); - Task task = (Task)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); - return await task.ConfigureAwait(false); + Task? task = (Task?)method.Invoke(null, [reader, context, arg.Split(','), services]); + if (task is not null) + return await task.ConfigureAwait(false); } - else - return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); + + return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); } return null; } } - private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) + private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) { TypeReaderResult readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); - return readResult.IsSuccess - ? readResult.BestMatch - : null; + return readResult.IsSuccess ? readResult.BestMatch : null; } private static async Task ReadMultiple(TypeReader reader, ICommandContext context, IEnumerable args, IServiceProvider services) { - List objs = new(); + List objs = []; foreach (string arg in args) { - object read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); - if (read != null) objs.Add((TObj)read); + object? read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); + if (read != null) + objs.Add((TObj)read); } return objs.ToImmutableArray(); diff --git a/src/Kook.Net.Commands/Readers/NullableTypeReader.cs b/src/Kook.Net.Commands/Readers/NullableTypeReader.cs index 9a5053c0..949e85ce 100644 --- a/src/Kook.Net.Commands/Readers/NullableTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/NullableTypeReader.cs @@ -7,7 +7,7 @@ internal static class NullableTypeReader public static TypeReader Create(Type type, TypeReader reader) { ConstructorInfo constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First(); - return (TypeReader)constructor.Invoke(new object[] { reader }); + return (TypeReader)constructor.Invoke([reader]); } } @@ -16,14 +16,17 @@ internal class NullableTypeReader : TypeReader { private readonly TypeReader _baseTypeReader; - public NullableTypeReader(TypeReader baseTypeReader) => _baseTypeReader = baseTypeReader; + public NullableTypeReader(TypeReader baseTypeReader) + { + _baseTypeReader = baseTypeReader; + } /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) - return TypeReaderResult.FromSuccess(new T?()); - + if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) + || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) + return TypeReaderResult.FromSuccess(new T()); return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs index 802fd66b..fe42a25f 100644 --- a/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -2,7 +2,7 @@ namespace Kook.Commands; internal static class PrimitiveTypeReader { - public static TypeReader Create(Type type) + public static TypeReader? Create(Type type) { type = typeof(PrimitiveTypeReader<>).MakeGenericType(type); return Activator.CreateInstance(type) as TypeReader; @@ -23,16 +23,16 @@ public PrimitiveTypeReader() /// must be within the range [0, 1]. public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { - if (score < 0 || score > 1) throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]."); - + if (score is < 0 or > 1) + throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]."); _tryParse = tryParse; _score = score; } public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - if (_tryParse(input, out T value)) return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); - + if (_tryParse(input, out T value)) + return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}.")); } } diff --git a/src/Kook.Net.Commands/Readers/RoleTypeReader.cs b/src/Kook.Net.Commands/Readers/RoleTypeReader.cs index 2c376ca3..59068865 100644 --- a/src/Kook.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/RoleTypeReader.cs @@ -24,7 +24,8 @@ public override Task ReadAsync(ICommandContext context, string }; //By Mention (1.0) - if (MentionUtils.TryParseRole(input, out uint id, tagMode)) AddResult(results, context.Guild.GetRole(id) as T, 1.00f); + if (MentionUtils.TryParseRole(input, out uint id, tagMode)) + AddResult(results, context.Guild.GetRole(id) as T, 1.00f); //By Id (0.9) if (uint.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) @@ -34,14 +35,16 @@ public override Task ReadAsync(ICommandContext context, string foreach (IRole role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase))) AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f); - if (results.Count > 0) return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); + if (results.Count > 0) + return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); } return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); } - private void AddResult(Dictionary results, T role, float score) + private void AddResult(Dictionary results, T? role, float score) { - if (role != null && !results.ContainsKey(role.Id)) results.Add(role.Id, new TypeReaderValue(role, score)); + if (role != null && !results.ContainsKey(role.Id)) + results.Add(role.Id, new TypeReaderValue(role, score)); } } diff --git a/src/Kook.Net.Commands/Readers/TimeSpanTypeReader.cs b/src/Kook.Net.Commands/Readers/TimeSpanTypeReader.cs index be55d33d..b604b9a4 100644 --- a/src/Kook.Net.Commands/Readers/TimeSpanTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/TimeSpanTypeReader.cs @@ -8,7 +8,7 @@ internal class TimeSpanTypeReader : TypeReader /// TimeSpan try parse formats. /// private static readonly string[] Formats = - { + [ "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s "%d'd'%h'h'%m'm'", // 4d3h2m "%d'd'%h'h'%s's'", // 4d3h 1s @@ -24,21 +24,25 @@ internal class TimeSpanTypeReader : TypeReader "%m'm'%s's'", // 2m1s "%m'm'", // 2m "%s's'" // 1s - }; + ]; /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - if (string.IsNullOrEmpty(input)) throw new ArgumentException($"{nameof(input)} must not be null or empty.", nameof(input)); + if (string.IsNullOrEmpty(input)) + throw new ArgumentException($"{nameof(input)} must not be null or empty.", nameof(input)); bool isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign - if (isNegative) input = input.Substring(1); + if (isNegative) + input = input[1..]; if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out TimeSpan timeSpan)) + { return isNegative ? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan)) : Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)); - else - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + } + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); } } diff --git a/src/Kook.Net.Commands/Readers/UserTypeReader.cs b/src/Kook.Net.Commands/Readers/UserTypeReader.cs index 4729dc4b..c6b38bd3 100644 --- a/src/Kook.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/UserTypeReader.cs @@ -14,8 +14,11 @@ public class UserTypeReader : TypeReader public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { Dictionary results = new(); - IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better - IReadOnlyCollection guildUsers = ImmutableArray.Create(); + List channelUsers = await context.Channel + .GetUsersAsync(CacheMode.CacheOnly) + .Flatten() + .ToListAsync() + .ConfigureAwait(false); // it's better TagMode tagMode = context.Message.Type switch { MessageType.Text => TagMode.PlainText, @@ -23,73 +26,79 @@ public override async Task ReadAsync(ICommandContext context, _ => throw new ArgumentOutOfRangeException(nameof(context.Message.Type)) }; - if (context.Guild != null) guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); + IReadOnlyCollection guildUsers = context.Guild != null + ? await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false) + : []; //By Mention (1.0) if (MentionUtils.TryParseUser(input, out ulong id, tagMode)) { - if (context.Guild != null) - AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); - else - AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); + T? user = context.Guild != null + ? await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T + : await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + AddResult(results, user, 1.00f); } //By Id (0.9) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - if (context.Guild != null) - AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); - else - AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); + T? user = context.Guild != null + ? await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T + : await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + AddResult(results, user, 0.90f); } //By Username + IdentifyNumber (0.7-0.85) int index = input.LastIndexOf('#'); if (index >= 0) { - string username = input.Substring(0, index); - if (ushort.TryParse(input.Substring(index + 1), out ushort identifyNumber)) + string username = input[..index]; + if (ushort.TryParse(input[(index + 1)..], out ushort identifyNumber)) { - IUser channelUser = await channelUsers.FirstOrDefaultAsync(x => - x.IdentifyNumberValue == identifyNumber && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)) - .ConfigureAwait(false); + IUser? channelUser = channelUsers.Find(x => + x.IdentifyNumberValue == identifyNumber + && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); - IGuildUser guildUser = guildUsers.FirstOrDefault(x => - x.IdentifyNumberValue == identifyNumber && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); + IGuildUser? guildUser = guildUsers.FirstOrDefault(x => + x.IdentifyNumberValue == identifyNumber + && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); } } //By Username (0.5-0.6) { - await channelUsers - .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) - .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)) - .ConfigureAwait(false); - - foreach (IGuildUser guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) - AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); + IEnumerable channelUserMatches = channelUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)); + foreach (IUser x in channelUserMatches) + AddResult(results, x as T, x.Username == input ? 0.65f : 0.55f); + IEnumerable guildUserMatches = guildUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)); + foreach (IGuildUser x in guildUserMatches) + AddResult(results, x as T, x.Username == input ? 0.60f : 0.50f); } //By Nickname (0.5-0.6) { - await channelUsers - .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) - .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)) - .ConfigureAwait(false); - - foreach (IGuildUser guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase))) - AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f); + IEnumerable channelUserMatches = channelUsers + .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)); + foreach (IUser x in channelUserMatches) + AddResult(results, x as T, (x as IGuildUser)?.Nickname == input ? 0.65f : 0.55f); + IEnumerable guildUserMatches = guildUsers + .Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase)); + foreach (IGuildUser x in guildUserMatches) + AddResult(results, x as T, x.Nickname == input ? 0.60f : 0.50f); } - if (results.Count > 0) return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); - + if (results.Count > 0) + return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); } - private void AddResult(Dictionary results, T user, float score) + private void AddResult(Dictionary results, T? user, float score) { - if (user != null && !results.ContainsKey(user.Id)) results.Add(user.Id, new TypeReaderValue(user, score)); + if (user != null && !results.ContainsKey(user.Id)) + results.Add(user.Id, new TypeReaderValue(user, score)); } } diff --git a/src/Kook.Net.Commands/Results/ExecuteResult.cs b/src/Kook.Net.Commands/Results/ExecuteResult.cs index 3d3c72a8..b16626d5 100644 --- a/src/Kook.Net.Commands/Results/ExecuteResult.cs +++ b/src/Kook.Net.Commands/Results/ExecuteResult.cs @@ -5,24 +5,24 @@ namespace Kook.Commands; /// /// Contains information of the command's overall execution result. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ExecuteResult : IResult { /// /// Gets the exception that may have occurred during the command execution. /// - public Exception Exception { get; } + public Exception? Exception { get; } /// public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// public bool IsSuccess => !Error.HasValue; - private ExecuteResult(Exception exception, CommandError? error, string errorReason) + private ExecuteResult(Exception? exception, CommandError? error, string? errorReason) { Exception = exception; Error = error; @@ -35,8 +35,7 @@ private ExecuteResult(Exception exception, CommandError? error, string errorReas /// /// A that does not contain any errors. /// - public static ExecuteResult FromSuccess() - => new(null, null, null); + public static ExecuteResult FromSuccess() => new(null, null, null); /// /// Initializes a new with a specified and its @@ -47,8 +46,7 @@ public static ExecuteResult FromSuccess() /// /// A that contains a and reason. /// - public static ExecuteResult FromError(CommandError error, string reason) - => new(null, error, reason); + public static ExecuteResult FromError(CommandError error, string reason) => new(null, error, reason); /// /// Initializes a new with a specified exception, indicating an unsuccessful @@ -60,8 +58,7 @@ public static ExecuteResult FromError(CommandError error, string reason) /// with a of type Exception as well as the exception message as the /// reason. /// - public static ExecuteResult FromError(Exception ex) - => new(ex, CommandError.Exception, ex.Message); + public static ExecuteResult FromError(Exception? ex) => new(ex, CommandError.Exception, ex?.Message); /// /// Initializes a new with a specified result; this may or may not be an @@ -72,8 +69,7 @@ public static ExecuteResult FromError(Exception ex) /// /// A that inherits the error type and reason. /// - public static ExecuteResult FromError(IResult result) - => new(null, result.Error, result.ErrorReason); + public static ExecuteResult FromError(IResult result) => new(null, result.Error, result.ErrorReason); /// /// Gets a string that indicates the execution result. diff --git a/src/Kook.Net.Commands/Results/IResult.cs b/src/Kook.Net.Commands/Results/IResult.cs index 41160fb5..d35e1d85 100644 --- a/src/Kook.Net.Commands/Results/IResult.cs +++ b/src/Kook.Net.Commands/Results/IResult.cs @@ -20,7 +20,7 @@ public interface IResult /// /// A string containing the error reason. /// - string ErrorReason { get; } + string? ErrorReason { get; } /// /// Indicates whether the operation was successful or not. diff --git a/src/Kook.Net.Commands/Results/MatchResult.cs b/src/Kook.Net.Commands/Results/MatchResult.cs index ed9162ab..f35b6ea1 100644 --- a/src/Kook.Net.Commands/Results/MatchResult.cs +++ b/src/Kook.Net.Commands/Results/MatchResult.cs @@ -16,18 +16,18 @@ public class MatchResult : IResult /// /// Gets on which pipeline stage the command may have matched or failed. /// - public IResult Pipeline { get; } + public IResult? Pipeline { get; } /// public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// public bool IsSuccess => !Error.HasValue; - private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, string errorReason) + private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string? errorReason) { Match = match; Error = error; @@ -41,8 +41,8 @@ private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, /// The command that matched. /// The pipeline stage on which the command matched. /// The match result. - public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) - => new(match, pipeline, null, null); + public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) => + new(match, pipeline, null, null); /// /// Creates a failed match result. @@ -50,24 +50,22 @@ public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) /// The error that occurred. /// The reason for the error. /// The match result. - public static MatchResult FromError(CommandError error, string reason) - => new(null, null, error, reason); + public static MatchResult FromError(CommandError error, string reason) => + new(null, null, error, reason); /// /// Creates a failed match result. /// /// The exception that occurred. /// The match result. - public static MatchResult FromError(Exception ex) - => FromError(CommandError.Exception, ex.Message); + public static MatchResult FromError(Exception ex) => FromError(CommandError.Exception, ex.Message); /// /// Creates a failed match result. /// /// The result that failed. /// The match result. - public static MatchResult FromError(IResult result) - => new(null, null, result.Error, result.ErrorReason); + public static MatchResult FromError(IResult result) => new(null, null, result.Error, result.ErrorReason); /// /// Creates a failed match result. @@ -76,8 +74,8 @@ public static MatchResult FromError(IResult result) /// The error that occurred. /// The reason for the error. /// The match result. - public static MatchResult FromError(IResult pipeline, CommandError error, string reason) - => new(null, pipeline, error, reason); + public static MatchResult FromError(IResult pipeline, CommandError error, string reason) => + new(null, pipeline, error, reason); /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; diff --git a/src/Kook.Net.Commands/Results/ParseResult.cs b/src/Kook.Net.Commands/Results/ParseResult.cs index af2ea510..1b3be29a 100644 --- a/src/Kook.Net.Commands/Results/ParseResult.cs +++ b/src/Kook.Net.Commands/Results/ParseResult.cs @@ -5,7 +5,7 @@ namespace Kook.Commands; /// /// Contains information for the parsing result from the command service's parser. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ParseResult : IResult { /// @@ -22,7 +22,7 @@ public struct ParseResult : IResult public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// /// Provides information about the parameter that caused the parsing error. @@ -31,13 +31,13 @@ public struct ParseResult : IResult /// A indicating the parameter info of the error that may have occurred during parsing; /// null if the parsing was successful or the parsing error is not specific to a single parameter. /// - public ParameterInfo ErrorParameter { get; } + public ParameterInfo? ErrorParameter { get; } /// public bool IsSuccess => !Error.HasValue; - private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, - string errorReason, ParameterInfo errorParamInfo) + private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, + CommandError? error, string? errorReason, ParameterInfo? errorParamInfo) { ArgValues = argValues; ParamValues = paramValues; @@ -54,14 +54,10 @@ private ParseResult(IReadOnlyList argValues, IReadOnlyList The parsing result. public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) { - for (int i = 0; i < argValues.Count; i++) - if (argValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); - - for (int i = 0; i < paramValues.Count; i++) - if (paramValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); - + if (argValues.Any(x => x.Values.Count > 1)) + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); + if (paramValues.Any(t => t.Values.Count > 1)) + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); return new ParseResult(argValues, paramValues, null, null, null); } @@ -73,16 +69,8 @@ public static ParseResult FromSuccess(IReadOnlyList argValues, /// The parsing result. public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) { - TypeReaderResult[] argList = new TypeReaderResult[argValues.Count]; - for (int i = 0; i < argValues.Count; i++) argList[i] = TypeReaderResult.FromSuccess(argValues[i]); - - TypeReaderResult[] paramList = null; - if (paramValues != null) - { - paramList = new TypeReaderResult[paramValues.Count]; - for (int i = 0; i < paramValues.Count; i++) paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); - } - + TypeReaderResult[] argList = [..argValues.Select(TypeReaderResult.FromSuccess)]; + TypeReaderResult[] paramList = [..paramValues.Select(TypeReaderResult.FromSuccess)]; return new ParseResult(argList, paramList, null, null, null); } @@ -92,8 +80,8 @@ public static ParseResult FromSuccess(IReadOnlyList argValues, /// The error that occurred. /// The reason for the error. /// The parsing result. - public static ParseResult FromError(CommandError error, string reason) - => new(null, null, error, reason, null); + public static ParseResult FromError(CommandError error, string reason) => + new([], [], error, reason, null); /// /// Creates a failed parsing result. @@ -102,24 +90,22 @@ public static ParseResult FromError(CommandError error, string reason) /// The reason for the error. /// The parameter info of the error that may have occurred during parsing. /// The parsing result. - public static ParseResult FromError(CommandError error, string reason, ParameterInfo parameterInfo) - => new(null, null, error, reason, parameterInfo); + public static ParseResult FromError(CommandError error, string reason, ParameterInfo parameterInfo) => + new([], [], error, reason, parameterInfo); /// /// Creates a failed parsing result. /// /// The exception that occurred. /// The parsing result. - public static ParseResult FromError(Exception ex) - => FromError(CommandError.Exception, ex.Message); + public static ParseResult FromError(Exception ex) => FromError(CommandError.Exception, ex.Message); /// /// Creates a failed parsing result. /// /// The result that contains the error. /// The parsing result. - public static ParseResult FromError(IResult result) - => new(null, null, result.Error, result.ErrorReason, null); + public static ParseResult FromError(IResult result) => new([], [], result.Error, result.ErrorReason, null); /// /// Creates a failed parsing result. @@ -127,8 +113,8 @@ public static ParseResult FromError(IResult result) /// The result that contains the error. /// The parameter info of the error that may have occurred during parsing. /// The parsing result. - public static ParseResult FromError(IResult result, ParameterInfo parameterInfo) - => new(null, null, result.Error, result.ErrorReason, parameterInfo); + public static ParseResult FromError(IResult result, ParameterInfo parameterInfo) => + new([], [], result.Error, result.ErrorReason, parameterInfo); /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; diff --git a/src/Kook.Net.Commands/Results/PreconditionGroupResult.cs b/src/Kook.Net.Commands/Results/PreconditionGroupResult.cs index fc1da50c..b50a8fd3 100644 --- a/src/Kook.Net.Commands/Results/PreconditionGroupResult.cs +++ b/src/Kook.Net.Commands/Results/PreconditionGroupResult.cs @@ -5,7 +5,7 @@ namespace Kook.Commands; /// /// Represents the result of a grouped precondition check. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class PreconditionGroupResult : PreconditionResult { /// @@ -19,37 +19,36 @@ public class PreconditionGroupResult : PreconditionResult /// The error that occurred. /// The reason for the error. /// The results of the precondition checks. - protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection preconditions) - : base(error, errorReason) => - PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); + protected PreconditionGroupResult(CommandError? error, string? errorReason, ICollection preconditions) + : base(error, errorReason) + { + PreconditionResults = [..preconditions]; + } /// /// Returns a with no errors. /// - public static new PreconditionGroupResult FromSuccess() - => new(null, null, null); + public static new PreconditionGroupResult FromSuccess() => new(null, null, []); /// /// Returns a with the reason and precondition results. /// /// The reason for the error. /// The results of the precondition checks. - public static PreconditionGroupResult FromError(string reason, ICollection preconditions) - => new(CommandError.UnmetPrecondition, reason, preconditions); + public static PreconditionGroupResult FromError(string reason, ICollection preconditions) => + new(CommandError.UnmetPrecondition, reason, preconditions); /// /// Returns a with an exception. /// /// The exception that occurred. - public static new PreconditionGroupResult FromError(Exception ex) - => new(CommandError.Exception, ex.Message, null); + public static new PreconditionGroupResult FromError(Exception ex) => new(CommandError.Exception, ex.Message, []); /// /// Returns a with the specified result. /// /// The result of failure. - public static new PreconditionGroupResult FromError(IResult result) //needed? - => new(result.Error, result.ErrorReason, null); + public static new PreconditionGroupResult FromError(IResult result) => new(result.Error, result.ErrorReason, []); //needed? /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; diff --git a/src/Kook.Net.Commands/Results/PreconditionResult.cs b/src/Kook.Net.Commands/Results/PreconditionResult.cs index be4b8c75..8cd72934 100644 --- a/src/Kook.Net.Commands/Results/PreconditionResult.cs +++ b/src/Kook.Net.Commands/Results/PreconditionResult.cs @@ -5,14 +5,14 @@ namespace Kook.Commands; /// /// Represents a result type for command preconditions. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class PreconditionResult : IResult { /// public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// public bool IsSuccess => !Error.HasValue; @@ -23,7 +23,7 @@ public class PreconditionResult : IResult /// /// The type of failure. /// The reason of failure. - protected PreconditionResult(CommandError? error, string errorReason) + protected PreconditionResult(CommandError? error, string? errorReason) { Error = error; ErrorReason = errorReason; @@ -32,30 +32,26 @@ protected PreconditionResult(CommandError? error, string errorReason) /// /// Returns a with no errors. /// - public static PreconditionResult FromSuccess() - => new(null, null); + public static PreconditionResult FromSuccess() => new(null, null); /// /// Returns a with and the /// specified reason. /// /// The reason of failure. - public static PreconditionResult FromError(string reason) - => new(CommandError.UnmetPrecondition, reason); + public static PreconditionResult FromError(string reason) => new(CommandError.UnmetPrecondition, reason); /// /// Returns a with an exception. /// /// The exception that occurred. - public static PreconditionResult FromError(Exception ex) - => new(CommandError.Exception, ex.Message); + public static PreconditionResult FromError(Exception ex) => new(CommandError.Exception, ex.Message); /// /// Returns a with the specified type. /// /// The result of failure. - public static PreconditionResult FromError(IResult result) - => new(result.Error, result.ErrorReason); + public static PreconditionResult FromError(IResult result) => new(result.Error, result.ErrorReason); /// /// Returns a string indicating whether the is successful. diff --git a/src/Kook.Net.Commands/Results/RuntimeResult.cs b/src/Kook.Net.Commands/Results/RuntimeResult.cs index e1d7dfa3..39b98069 100644 --- a/src/Kook.Net.Commands/Results/RuntimeResult.cs +++ b/src/Kook.Net.Commands/Results/RuntimeResult.cs @@ -5,7 +5,7 @@ namespace Kook.Commands; /// /// Represents the runtime result of a command execution. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class RuntimeResult : IResult { /// @@ -29,7 +29,7 @@ protected RuntimeResult(CommandError? error, string reason) public bool IsSuccess => !Error.HasValue; /// - string IResult.ErrorReason => Reason; + string? IResult.ErrorReason => Reason; /// public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); diff --git a/src/Kook.Net.Commands/Results/SearchResult.cs b/src/Kook.Net.Commands/Results/SearchResult.cs index d6682704..8872d406 100644 --- a/src/Kook.Net.Commands/Results/SearchResult.cs +++ b/src/Kook.Net.Commands/Results/SearchResult.cs @@ -5,13 +5,13 @@ namespace Kook.Commands; /// /// Represents the result of a command search. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct SearchResult : IResult { /// /// Gets the text that was searched in. /// - public string Text { get; } + public string? Text { get; } /// /// Gets the commands that were found. @@ -22,12 +22,12 @@ public struct SearchResult : IResult public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// public bool IsSuccess => !Error.HasValue; - private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) + private SearchResult(string? text, IReadOnlyList commands, CommandError? error, string? errorReason) { Text = text; Commands = commands; @@ -40,8 +40,8 @@ private SearchResult(string text, IReadOnlyList commands, CommandE /// /// The text that was searched in. /// The commands that were found. - public static SearchResult FromSuccess(string text, IReadOnlyList commands) - => new(text, commands, null, null); + public static SearchResult FromSuccess(string text, IReadOnlyList commands) => + new(text, commands, null, null); /// /// Returns a with a . @@ -49,25 +49,22 @@ public static SearchResult FromSuccess(string text, IReadOnlyList /// The type of failure. /// The reason of failure. /// - public static SearchResult FromError(CommandError error, string reason) - => new(null, null, error, reason); + public static SearchResult FromError(CommandError error, string reason) => new(null, [], error, reason); /// /// Returns a with an exception. /// /// The exception that occurred. - public static SearchResult FromError(Exception ex) - => FromError(CommandError.Exception, ex.Message); + public static SearchResult FromError(Exception ex) => FromError(CommandError.Exception, ex.Message); /// /// Returns a with the specified type. /// /// The result of failure. - public static SearchResult FromError(IResult result) - => new(null, null, result.Error, result.ErrorReason); + public static SearchResult FromError(IResult result) => new(null, [], result.Error, result.ErrorReason); /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; - private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({Commands?.Count ?? 0} Results)" : $"{Error}: {ErrorReason}"; } diff --git a/src/Kook.Net.Commands/Results/TypeReaderResult.cs b/src/Kook.Net.Commands/Results/TypeReaderResult.cs index 45377763..ec0854ca 100644 --- a/src/Kook.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Kook.Net.Commands/Results/TypeReaderResult.cs @@ -6,13 +6,13 @@ namespace Kook.Commands; /// /// Represents a parsing result of a type reader. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct TypeReaderValue { /// /// Gets the parsed value. /// - public object Value { get; } + public object? Value { get; } /// /// Gets the confidence score of the parsing. @@ -24,14 +24,14 @@ public struct TypeReaderValue /// /// The parsed value. /// The confidence score of the parsing. - public TypeReaderValue(object value, float score) + public TypeReaderValue(object? value, float score) { Value = value; Score = score; } /// - public override string ToString() => Value?.ToString(); + public override string? ToString() => Value?.ToString(); private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]"; } @@ -39,7 +39,7 @@ public TypeReaderValue(object value, float score) /// /// Represents a parsing result of a type reader. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct TypeReaderResult : IResult { /// @@ -51,21 +51,17 @@ public struct TypeReaderResult : IResult public CommandError? Error { get; } /// - public string ErrorReason { get; } + public string? ErrorReason { get; } /// public bool IsSuccess => !Error.HasValue; /// TypeReaderResult was not successful. - public object BestMatch => IsSuccess -#if NET5_0_OR_GREATER - ? Values.Count == 1 ? Values.Single().Value : Values.MaxBy(v => v.Score).Value -#else - ? Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value -#endif + public object? BestMatch => IsSuccess + ? Values?.MaxBy(v => v.Score).Value : throw new InvalidOperationException("TypeReaderResult was not successful."); - private TypeReaderResult(IReadOnlyCollection values, CommandError? error, string errorReason) + private TypeReaderResult(IReadOnlyCollection values, CommandError? error, string? errorReason) { Values = values; Error = error; @@ -76,47 +72,43 @@ private TypeReaderResult(IReadOnlyCollection values, CommandErr /// Returns a with no errors. /// /// The parsed value. - public static TypeReaderResult FromSuccess(object value) - => new(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null); + public static TypeReaderResult FromSuccess(object? value) => + new(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null); /// /// Returns a with no errors. /// /// The parsed value. - public static TypeReaderResult FromSuccess(TypeReaderValue value) - => new(ImmutableArray.Create(value), null, null); + public static TypeReaderResult FromSuccess(TypeReaderValue value) => + new(ImmutableArray.Create(value), null, null); /// /// Returns a with no errors. /// /// The parsed values. - public static TypeReaderResult FromSuccess(IReadOnlyCollection values) - => new(values, null, null); + public static TypeReaderResult FromSuccess(IReadOnlyCollection values) => new(values, null, null); /// /// Returns a with a specified error. /// /// The error. /// The reason for the error. - public static TypeReaderResult FromError(CommandError error, string reason) - => new(null, error, reason); + public static TypeReaderResult FromError(CommandError error, string reason) => new([], error, reason); /// /// Returns a with an exception. /// /// The exception that occurred. - public static TypeReaderResult FromError(Exception ex) - => FromError(CommandError.Exception, ex.Message); + public static TypeReaderResult FromError(Exception ex) => FromError(CommandError.Exception, ex.Message); /// - /// Returns a with an specified result. + /// Returns a with a specified result. /// /// The result. - public static TypeReaderResult FromError(IResult result) - => new(null, result.Error, result.ErrorReason); + public static TypeReaderResult FromError(IResult result) => new([], result.Error, result.ErrorReason); /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; - private string DebuggerDisplay => IsSuccess ? $"Success ({string.Join(", ", Values)})" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({string.Join(", ", Values ?? [])})" : $"{Error}: {ErrorReason}"; } diff --git a/src/Kook.Net.Commands/Utilities/QuotationAliasUtils.cs b/src/Kook.Net.Commands/Utilities/QuotationAliasUtils.cs index 75fbdb57..5788f643 100644 --- a/src/Kook.Net.Commands/Utilities/QuotationAliasUtils.cs +++ b/src/Kook.Net.Commands/Utilities/QuotationAliasUtils.cs @@ -11,7 +11,7 @@ internal static class QuotationAliasUtils /// Used in the . /// /// - internal static Dictionary GetDefaultAliasMap => + internal static Dictionary DefaultAliasMap => // Output of a gist provided by https://gist.github.com/ufcpp // https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5 // This was not used for the implementation because of incompatibility with netstandard1.1 diff --git a/src/Kook.Net.Commands/Utilities/ReflectionUtils.cs b/src/Kook.Net.Commands/Utilities/ReflectionUtils.cs index 2f4b0503..450f3639 100644 --- a/src/Kook.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Kook.Net.Commands/Utilities/ReflectionUtils.cs @@ -6,8 +6,8 @@ internal static class ReflectionUtils { private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); - internal static T CreateObject(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) - => CreateBuilder(typeInfo, commands)(services); + internal static T CreateObject(TypeInfo typeInfo, CommandService commands, IServiceProvider services) => + CreateBuilder(typeInfo, commands)(services); internal static Func CreateBuilder(TypeInfo typeInfo, CommandService commands) { @@ -15,15 +15,14 @@ internal static Func CreateBuilder(TypeInfo typeInfo, Co System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); PropertyInfo[] properties = GetProperties(typeInfo); - return (services) => + return services => { - object[] args = new object[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo); + object[] args = parameters + .Select(x => GetMember(commands, services, x.ParameterType, typeInfo)) + .ToArray(); T obj = InvokeConstructor(constructor, args, typeInfo); - foreach (PropertyInfo property in properties) property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo)); - return obj; }; } @@ -43,26 +42,26 @@ private static T InvokeConstructor(ConstructorInfo constructor, object[] args private static ConstructorInfo GetConstructor(TypeInfo ownerType) { ConstructorInfo[] constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); - if (constructors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"."); - else if (constructors.Length > 1) throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"."); - - return constructors[0]; + return constructors.Length switch + { + 0 => throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"."), + > 1 => throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"."), + _ => constructors[0] + }; } - private static PropertyInfo[] GetProperties(TypeInfo ownerType) + private static PropertyInfo[] GetProperties(TypeInfo? ownerType) { - List result = new(); + List result = []; while (ownerType != ObjectTypeInfo) { - foreach (PropertyInfo prop in ownerType.DeclaredProperties) + foreach (PropertyInfo prop in ownerType?.DeclaredProperties ?? []) { - if (prop.SetMethod?.IsStatic == false - && prop.SetMethod?.IsPublic == true - && prop.GetCustomAttribute() == null) result.Add(prop); + if (prop.SetMethod is { IsStatic: false, IsPublic: true } + && prop.GetCustomAttribute() == null) + result.Add(prop); } - - ownerType = ownerType.BaseType.GetTypeInfo(); + ownerType = ownerType?.BaseType?.GetTypeInfo(); } return result.ToArray(); @@ -71,12 +70,9 @@ private static PropertyInfo[] GetProperties(TypeInfo ownerType) private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType) { if (memberType == typeof(CommandService)) return commands; - if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) return services; - - object service = services.GetService(memberType); + object? service = services.GetService(memberType); if (service != null) return service; - throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found."); } } diff --git a/src/Kook.Net.Core/AssemblyInfo.cs b/src/Kook.Net.Core/AssemblyInfo.cs index 839a22de..61eaf444 100644 --- a/src/Kook.Net.Core/AssemblyInfo.cs +++ b/src/Kook.Net.Core/AssemblyInfo.cs @@ -3,6 +3,7 @@ [assembly: InternalsVisibleTo("Kook.Net.Rest")] [assembly: InternalsVisibleTo("Kook.Net.Experimental")] [assembly: InternalsVisibleTo("Kook.Net.WebSocket")] +[assembly: InternalsVisibleTo("Kook.Net.CardMarkup")] [assembly: InternalsVisibleTo("Kook.Net.Commands")] [assembly: InternalsVisibleTo("Kook.Net.Tests.Unit")] [assembly: InternalsVisibleTo("Kook.Net.Tests.Integration")] diff --git a/src/Kook.Net.Core/Audio/AudioStream.cs b/src/Kook.Net.Core/Audio/AudioStream.cs index e48ba964..4e32e10a 100644 --- a/src/Kook.Net.Core/Audio/AudioStream.cs +++ b/src/Kook.Net.Core/Audio/AudioStream.cs @@ -49,7 +49,7 @@ public void Clear() /// /// The cancellation token to be used. /// A task that represents an asynchronous clear operation. - public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } + public virtual Task ClearAsync(CancellationToken cancellationToken) => Task.Delay(0); /// /// Reading stream length is not supported. diff --git a/src/Kook.Net.Core/Audio/IAudioClient.cs b/src/Kook.Net.Core/Audio/IAudioClient.cs index 0e60478b..455f2ffd 100644 --- a/src/Kook.Net.Core/Audio/IAudioClient.cs +++ b/src/Kook.Net.Core/Audio/IAudioClient.cs @@ -1,5 +1,3 @@ -using System.Net; - namespace Kook.Audio; /// diff --git a/src/Kook.Net.Core/Commands/ICommandContext.cs b/src/Kook.Net.Core/Commands/ICommandContext.cs index 5b2fce01..2fbe4380 100644 --- a/src/Kook.Net.Core/Commands/ICommandContext.cs +++ b/src/Kook.Net.Core/Commands/ICommandContext.cs @@ -13,7 +13,7 @@ public interface ICommandContext /// /// Gets the that the command is executed in. /// - IGuild Guild { get; } + IGuild? Guild { get; } /// /// Gets the that the command is executed in. diff --git a/src/Kook.Net.Core/Entities/Activities/GameProperties.cs b/src/Kook.Net.Core/Entities/Activities/GameProperties.cs index a8463cbd..b9ee8711 100644 --- a/src/Kook.Net.Core/Entities/Activities/GameProperties.cs +++ b/src/Kook.Net.Core/Entities/Activities/GameProperties.cs @@ -9,10 +9,10 @@ public class GameProperties /// /// Gets or sets the name of the game. /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Gets or sets the icon URL of the game. /// - public string IconUrl { get; set; } + public string? IconUrl { get; set; } } diff --git a/src/Kook.Net.Core/Entities/Activities/IActivity.cs b/src/Kook.Net.Core/Entities/Activities/IActivity.cs index df12faa6..aba71070 100644 --- a/src/Kook.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Kook.Net.Core/Entities/Activities/IActivity.cs @@ -3,6 +3,4 @@ namespace Kook; /// /// A user's activity status. /// -public interface IActivity -{ -} +public interface IActivity; diff --git a/src/Kook.Net.Core/Entities/Activities/IGame.cs b/src/Kook.Net.Core/Entities/Activities/IGame.cs index ba281a6c..bb009625 100644 --- a/src/Kook.Net.Core/Entities/Activities/IGame.cs +++ b/src/Kook.Net.Core/Entities/Activities/IGame.cs @@ -27,7 +27,7 @@ public interface IGame : IActivity, IEntity, IDeletable /// /// A string containing the additional information about the game. /// - string Options { get; } + string? Options { get; } /// /// Gets whether the Kook client needs administrator privileges to detect the game. @@ -59,7 +59,7 @@ public interface IGame : IActivity, IEntity, IDeletable /// /// A string representing the URL of the game's icon. /// - string Icon { get; } + string? Icon { get; } /// /// Modifies this game. @@ -73,5 +73,5 @@ public interface IGame : IActivity, IEntity, IDeletable /// /// A task that represents the asynchronous modification operation. /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Activities/Music.cs b/src/Kook.Net.Core/Entities/Activities/Music.cs index 4c112de5..3404bbd3 100644 --- a/src/Kook.Net.Core/Entities/Activities/Music.cs +++ b/src/Kook.Net.Core/Entities/Activities/Music.cs @@ -13,10 +13,10 @@ public class Music /// /// Gets or sets the music ID. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the singer. /// - public string Singer { get; set; } + public string? Singer { get; set; } } diff --git a/src/Kook.Net.Core/Entities/Channels/CreateCategoryChannelProperties.cs b/src/Kook.Net.Core/Entities/Channels/CreateCategoryChannelProperties.cs index 04f6416d..05f89b77 100644 --- a/src/Kook.Net.Core/Entities/Channels/CreateCategoryChannelProperties.cs +++ b/src/Kook.Net.Core/Entities/Channels/CreateCategoryChannelProperties.cs @@ -4,6 +4,4 @@ namespace Kook; /// Provides properties that are used to create an with the specified properties. /// /// -public class CreateCategoryChannelProperties : CreateGuildChannelProperties -{ -} +public class CreateCategoryChannelProperties : CreateGuildChannelProperties; diff --git a/src/Kook.Net.Core/Entities/Channels/CreateGuildChannelProperties.cs b/src/Kook.Net.Core/Entities/Channels/CreateGuildChannelProperties.cs index 12f146f9..3f78d236 100644 --- a/src/Kook.Net.Core/Entities/Channels/CreateGuildChannelProperties.cs +++ b/src/Kook.Net.Core/Entities/Channels/CreateGuildChannelProperties.cs @@ -3,6 +3,4 @@ namespace Kook; /// /// Properties that are used to create an with the specified properties. /// -public class CreateGuildChannelProperties -{ -} +public class CreateGuildChannelProperties; diff --git a/src/Kook.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Kook.Net.Core/Entities/Channels/IAudioChannel.cs index 4fc8d07f..cf4f28ef 100644 --- a/src/Kook.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IAudioChannel.cs @@ -20,7 +20,7 @@ public interface IAudioChannel : IChannel /// This property may be empty if the voice channel is created before this feature was released. /// /// - string VoiceRegion { get; } + string? VoiceRegion { get; } /// /// Gets the server url that clients should connect to to join this voice channel. @@ -28,7 +28,7 @@ public interface IAudioChannel : IChannel /// /// A string representing the url that clients should connect to to join this voice channel. /// - string ServerUrl { get; } + string? ServerUrl { get; } // /// Determines whether the client should deaf itself upon connection. // /// Determines whether the client should mute itself upon connection. @@ -41,7 +41,8 @@ public interface IAudioChannel : IChannel /// A task representing the asynchronous connection operation. The task result contains the /// responsible for the connection. /// - Task ConnectAsync(/*bool selfDeaf = false, bool selfMute = false, */bool external = false, bool disconnect = true); + Task ConnectAsync( /*bool selfDeaf = false, bool selfMute = false, */ + bool external = false, bool disconnect = true); /// /// Disconnects from this audio channel. diff --git a/src/Kook.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Kook.Net.Core/Entities/Channels/ICategoryChannel.cs index f0f73ef9..4501dc8d 100644 --- a/src/Kook.Net.Core/Entities/Channels/ICategoryChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -3,6 +3,4 @@ namespace Kook; /// /// Represents a generic category channel. /// -public interface ICategoryChannel : IGuildChannel -{ -} +public interface ICategoryChannel : IGuildChannel; diff --git a/src/Kook.Net.Core/Entities/Channels/IChannel.cs b/src/Kook.Net.Core/Entities/Channels/IChannel.cs index 09058adc..a3d9d241 100644 --- a/src/Kook.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IChannel.cs @@ -39,7 +39,7 @@ public interface IChannel : IEntity /// /// Paged collection of users. /// - IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a user in this channel. @@ -51,7 +51,7 @@ public interface IChannel : IEntity /// A task that represents the asynchronous get operation. The task result contains a user object that /// represents the found user; null if none is found. /// - Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Channels/IDMChannel.cs b/src/Kook.Net.Core/Entities/Channels/IDMChannel.cs index 637bdbe7..df6a8637 100644 --- a/src/Kook.Net.Core/Entities/Channels/IDMChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IDMChannel.cs @@ -38,7 +38,7 @@ public interface IDMChannel : IMessageChannel, IPrivateChannel, IEntity /// /// A task that represents the asynchronous close operation. /// - Task CloseAsync(RequestOptions options = null); + Task CloseAsync(RequestOptions? options = null); #endregion @@ -51,8 +51,8 @@ public interface IDMChannel : IMessageChannel, IPrivateChannel, IEntity /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendFileAsync(string path, string fileName = null, - AttachmentType type = AttachmentType.File, IQuote quote = null, RequestOptions options = null); + Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null); /// /// Sends a file to this message channel. @@ -61,8 +61,8 @@ Task> SendFileAsync(string path, string fileName = /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendFileAsync(Stream stream, string fileName = null, - AttachmentType type = AttachmentType.File, IQuote quote = null, RequestOptions options = null); + Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null); /// /// Sends a file to this message channel. @@ -71,8 +71,7 @@ Task> SendFileAsync(Stream stream, string fileName /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendFileAsync(FileAttachment attachment, - IQuote quote = null, RequestOptions options = null); + Task> SendFileAsync(FileAttachment attachment, IQuote? quote = null, RequestOptions? options = null); /// /// Sends a text message to this message channel. @@ -81,8 +80,7 @@ Task> SendFileAsync(FileAttachment attachment, /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendTextAsync(string text, IQuote quote = null, - RequestOptions options = null); + Task> SendTextAsync(string text, IQuote? quote = null, RequestOptions? options = null); /// /// Sends a card message to this message channel. @@ -91,8 +89,7 @@ Task> SendTextAsync(string text, IQuote quote = nu /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendCardAsync(ICard card, - IQuote quote = null, RequestOptions options = null); + Task> SendCardAsync(ICard card, IQuote? quote = null, RequestOptions? options = null); /// /// Sends a card message to this message channel. @@ -101,8 +98,7 @@ Task> SendCardAsync(ICard card, /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendCardsAsync(IEnumerable cards, - IQuote quote = null, RequestOptions options = null); + Task> SendCardsAsync(IEnumerable cards, IQuote? quote = null, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Kook.Net.Core/Entities/Channels/IGuildChannel.cs index 78d09466..a3dcd37d 100644 --- a/src/Kook.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IGuildChannel.cs @@ -77,7 +77,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task that represents the asynchronous modification operation. /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); /// /// Gets the creator of this channel. @@ -87,7 +87,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task that represents the asynchronous get operation. The task result contains the creator of this channel. /// - Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -119,7 +119,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); + Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null); /// /// Removes the permission overwrite for the given user, if one exists. @@ -129,7 +129,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null); + Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null); /// /// Adds the permission overwrite for the given role. @@ -140,7 +140,7 @@ public interface IGuildChannel : IChannel, IDeletable /// A task representing the asynchronous permission operation for adding the specified permissions to the /// channel. /// - Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null); + Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null); /// /// Adds the permission overwrite for the given user. @@ -150,7 +150,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null); + Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null); /// /// Updates the permission overwrite for the given role. @@ -162,7 +162,7 @@ public interface IGuildChannel : IChannel, IDeletable /// A task representing the asynchronous permission operation for adding the specified permissions to the /// channel. /// - Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null); + Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions? options = null); /// /// Updates the permission overwrite for the given user. @@ -173,7 +173,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, RequestOptions options = null); + Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, RequestOptions? options = null); #endregion @@ -191,7 +191,7 @@ public interface IGuildChannel : IChannel, IDeletable /// /// Paged collection of users. /// - new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a user in this channel. @@ -203,7 +203,7 @@ public interface IGuildChannel : IChannel, IDeletable /// A task representing the asynchronous get operation. The task result contains a guild user object that /// represents the user; null if none is found. /// - new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Kook.Net.Core/Entities/Channels/IMessageChannel.cs index 82dc4f95..17d712ef 100644 --- a/src/Kook.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IMessageChannel.cs @@ -14,7 +14,7 @@ public interface IMessageChannel : IChannel /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The file path of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The user only who can see the message. Leave null to let everyone see the message. @@ -23,9 +23,9 @@ public interface IMessageChannel : IChannel /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendFileAsync(string path, string fileName = null, - AttachmentType type = AttachmentType.File, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null); + Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null); /// /// Sends a file to this message channel. @@ -34,7 +34,7 @@ Task> SendFileAsync(string path, string fileName = /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The stream of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The user only who can see the message. Leave null to let everyone see the message. @@ -43,9 +43,9 @@ Task> SendFileAsync(string path, string fileName = /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendFileAsync(Stream stream, string fileName, - AttachmentType type = AttachmentType.File, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null); + Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null); /// /// Sends a file to this message channel. @@ -62,7 +62,7 @@ Task> SendFileAsync(Stream stream, string fileName /// contains the identifier and timestamp of the sent message. /// Task> SendFileAsync(FileAttachment attachment, - IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null); + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null); /// /// Sends a text message to this message channel. @@ -75,8 +75,8 @@ Task> SendFileAsync(FileAttachment attachment, /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - Task> SendTextAsync(string text, IQuote quote = null, - IUser ephemeralUser = null, RequestOptions options = null); + Task> SendTextAsync(string text, IQuote? quote = null, + IUser? ephemeralUser = null, RequestOptions? options = null); /// /// Sends a card message to this message channel. @@ -90,7 +90,7 @@ Task> SendTextAsync(string text, IQuote quote = nu /// contains the identifier and timestamp of the sent message. /// Task> SendCardAsync(ICard card, - IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null); + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null); /// /// Sends a card message to this message channel. @@ -104,7 +104,7 @@ Task> SendCardAsync(ICard card, /// contains the identifier and timestamp of the sent message. /// Task> SendCardsAsync(IEnumerable cards, - IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null); + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null); /// /// Gets a message from this message channel. @@ -116,7 +116,7 @@ Task> SendCardsAsync(IEnumerable cards, /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - Task GetMessageAsync(Guid id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetMessageAsync(Guid id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -144,13 +144,13 @@ Task> SendCardsAsync(IEnumerable cards, /// /// The numbers of message to be gotten from. /// The that determines whether the object should be fetched from - /// cache. + /// cache. /// The options to be used when sending the request. /// /// Paged collection of messages. /// IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a collection of messages in this channel. @@ -177,14 +177,14 @@ IAsyncEnumerable> GetMessagesAsync(int limit = Koo /// The direction of the messages to be gotten from. /// The numbers of message to be gotten from. /// The that determines whether the object should be fetched from - /// cache. + /// cache. /// The options to be used when sending the request. /// /// Paged collection of messages. /// IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a collection of messages in this channel. @@ -211,14 +211,14 @@ IAsyncEnumerable> GetMessagesAsync(Guid referenceM /// The direction of the messages to be gotten from. /// The numbers of message to be gotten from. /// The that determines whether the object should be fetched from - /// cache. + /// cache. /// The options to be used when sending the request. /// /// Paged collection of messages. /// IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -232,7 +232,7 @@ IAsyncEnumerable> GetMessagesAsync(IMessage refere /// /// A task that represents the asynchronous removal operation. /// - Task DeleteMessageAsync(Guid messageId, RequestOptions options = null); + Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null); /// Deletes a message based on the provided message in this channel. /// The message that would be removed. @@ -240,7 +240,7 @@ IAsyncEnumerable> GetMessagesAsync(IMessage refere /// /// A task that represents the asynchronous removal operation. /// - Task DeleteMessageAsync(IMessage message, RequestOptions options = null); + Task DeleteMessageAsync(IMessage message, RequestOptions? options = null); #endregion @@ -259,7 +259,7 @@ IAsyncEnumerable> GetMessagesAsync(IMessage refere /// /// A task that represents the asynchronous modification operation. /// - Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null); + Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Channels/INestedChannel.cs b/src/Kook.Net.Core/Entities/Channels/INestedChannel.cs index 4ee072a8..035efc53 100644 --- a/src/Kook.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/INestedChannel.cs @@ -31,7 +31,7 @@ public interface INestedChannel : IGuildChannel /// /// A task that represents the asynchronous operation for syncing channel permissions with its parent's. /// - Task SyncPermissionsAsync(RequestOptions options = null); + Task SyncPermissionsAsync(RequestOptions? options = null); /// /// Gets the parent (category) channel of this channel. @@ -42,7 +42,7 @@ public interface INestedChannel : IGuildChannel /// A task that represents the asynchronous get operation. The task result contains the category channel /// representing the parent of this channel; null if none is set. /// - Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -56,7 +56,7 @@ public interface INestedChannel : IGuildChannel /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// invite, each representing information for an invite found within this guild. /// - Task> GetInvitesAsync(RequestOptions options = null); + Task> GetInvitesAsync(RequestOptions? options = null); /// /// Creates a new invite to this channel. @@ -68,8 +68,9 @@ public interface INestedChannel : IGuildChannel /// A task that represents the asynchronous invite creation operation. The task result contains an invite /// metadata object containing information for the created invite. /// - Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null); + Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, + RequestOptions? options = null); /// /// Creates a new invite to this channel. @@ -81,7 +82,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// A task that represents the asynchronous invite creation operation. The task result contains an invite /// metadata object containing information for the created invite. /// - Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null); + Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Channels/ITextChannel.cs b/src/Kook.Net.Core/Entities/Channels/ITextChannel.cs index 34e5a61b..bca186ad 100644 --- a/src/Kook.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/ITextChannel.cs @@ -32,7 +32,7 @@ public interface ITextChannel : INestedChannel, IMentionable, IMessageChannel /// A task that represents the asynchronous modification operation. /// /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); #endregion @@ -44,5 +44,5 @@ public interface ITextChannel : INestedChannel, IMentionable, IMessageChannel /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. /// The task result contains a collection of messages found in the pinned messages. /// - Task> GetPinnedMessagesAsync(RequestOptions options = null); + Task> GetPinnedMessagesAsync(RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Kook.Net.Core/Entities/Channels/IVoiceChannel.cs index 2a853da2..2f26e122 100644 --- a/src/Kook.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Kook.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -19,9 +19,9 @@ public interface IVoiceChannel : ITextChannel, IAudioChannel /// /// /// An int representing the maximum number of users that are allowed to be connected to this - /// channel at once; null if a limit is not set. + /// channel at once; 0 if a limit is not set. /// - int? UserLimit { get; } + int UserLimit { get; } /// /// Gets whether this voice channel is locked by a password. @@ -40,7 +40,7 @@ public interface IVoiceChannel : ITextChannel, IAudioChannel /// A task that represents the asynchronous modification operation. /// /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); /// /// Gets the users connected to this voice channel. @@ -51,5 +51,5 @@ public interface IVoiceChannel : ITextChannel, IAudioChannel /// A task that represents the asynchronous get operation. The task result contains a collection of /// s that are connected to this voice channel. /// - Task> GetConnectedUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetConnectedUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Channels/ModifyGuildChannelProperties.cs b/src/Kook.Net.Core/Entities/Channels/ModifyGuildChannelProperties.cs index 8de57793..051a39ab 100644 --- a/src/Kook.Net.Core/Entities/Channels/ModifyGuildChannelProperties.cs +++ b/src/Kook.Net.Core/Entities/Channels/ModifyGuildChannelProperties.cs @@ -13,7 +13,7 @@ public class ModifyGuildChannelProperties /// This property defines the new name for this channel; /// if this is null, the name will not be modified. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Moves the channel to the following position. This property is one-based. diff --git a/src/Kook.Net.Core/Entities/Channels/ModifyTextChannelProperties.cs b/src/Kook.Net.Core/Entities/Channels/ModifyTextChannelProperties.cs index 453c5c5a..f39f960e 100644 --- a/src/Kook.Net.Core/Entities/Channels/ModifyTextChannelProperties.cs +++ b/src/Kook.Net.Core/Entities/Channels/ModifyTextChannelProperties.cs @@ -13,7 +13,7 @@ public class ModifyTextChannelProperties : ModifyGuildChannelProperties /// Setting this value to any string other than null or will set the /// channel topic or description to the desired value. /// - public string Topic { get; set; } + public string? Topic { get; set; } /// /// Gets or sets the slow-mode ratelimit in seconds for this channel. @@ -23,7 +23,7 @@ public class ModifyTextChannelProperties : ModifyGuildChannelProperties /// will disable slow-mode for this channel; /// if this value is set to null, the slow-mode interval will not be modified. /// - /// Users with or + /// Users with or /// will be exempt from slow-mode. /// /// diff --git a/src/Kook.Net.Core/Entities/Channels/ModifyVoiceChannelProperties.cs b/src/Kook.Net.Core/Entities/Channels/ModifyVoiceChannelProperties.cs index a7d7d905..abf737b4 100644 --- a/src/Kook.Net.Core/Entities/Channels/ModifyVoiceChannelProperties.cs +++ b/src/Kook.Net.Core/Entities/Channels/ModifyVoiceChannelProperties.cs @@ -29,7 +29,7 @@ public class ModifyVoiceChannelProperties : ModifyTextChannelProperties /// /// Gets or sets the password of the channel, or empty string to clear the password; null if not set. /// - public string Password { get; set; } + public string? Password { get; set; } /// /// Gets or sets a value that indicates whether the voice region of the channel is overwritten; @@ -40,5 +40,5 @@ public class ModifyVoiceChannelProperties : ModifyTextChannelProperties /// /// Gets or sets the voice region of the channel; null if not set. /// - public string VoiceRegion { get; set; } + public string? VoiceRegion { get; set; } } diff --git a/src/Kook.Net.Core/Entities/Emotes/Emoji.cs b/src/Kook.Net.Core/Entities/Emotes/Emoji.cs index f2421911..44567c25 100644 --- a/src/Kook.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Kook.Net.Core/Entities/Emotes/Emoji.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -14,8 +15,6 @@ public class Emoji : IEmote /// public string Id => Name; - // TODO: HTML codepoints - /// /// Gets the Unicode representation of this emoji. /// @@ -28,43 +27,45 @@ public class Emoji : IEmote /// Initializes a new class with the provided Unicode. /// /// The pure UTF-8 encoding of an emoji. - public Emoji(string unicode) => Name = TryParseAsUnicodePoint(unicode, out string name) ? name : unicode; + public Emoji(string unicode) + { + Name = TryParseAsUnicodePoint(unicode, out string? name) ? name : unicode; + } /// /// Determines whether the specified emoji is equal to the current one. /// /// The object to compare with the current object. - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { if (obj == null) return false; - if (obj == this) return true; - return obj is Emoji otherEmoji && string.Equals(Name, otherEmoji.Name); } /// Tries to parse an from its raw format. /// The raw encoding of an emoji. For example: :heart: or ❤ /// An emoji. - public static bool TryParse(string text, out Emoji result) + public static bool TryParse([NotNullWhen(true)] string? text, + [NotNullWhen(true)] out Emoji? result) { result = null; - if (string.IsNullOrWhiteSpace(text)) return false; - - if (NamesAndUnicodes.ContainsKey(text)) result = new Emoji(NamesAndUnicodes[text]); - - if (Unicodes.Contains(text)) result = new Emoji(text); - + if (text is null || string.IsNullOrWhiteSpace(text)) + return false; + if (NamesAndUnicodes.TryGetValue(text, out string? nameUnicode)) + result = new Emoji(nameUnicode); + if (Unicodes.Contains(text)) + result = new Emoji(text); return result != null; } /// Parse an from its raw format. /// The raw encoding of an emoji. For example: :heart: or ❤ /// String is not emoji or unicode! - public static Emoji Parse(string emojiStr) + public static Emoji Parse([NotNull] string? emojiStr) { - if (!TryParse(emojiStr, out Emoji emoji)) throw new FormatException("String is not emoji name or unicode."); - + if (!TryParse(emojiStr, out Emoji? emoji)) + throw new FormatException("String is not emoji name or unicode."); return emoji; } @@ -72,15 +73,11 @@ public static Emoji Parse(string emojiStr) /// Try parsing an from its unicode point format. /// For example: [#128187;] -> 💻 /// - internal bool TryParseAsUnicodePoint(string unicodePoint, out string name) + internal static bool TryParseAsUnicodePoint(string unicodePoint, [NotNullWhen(true)] out string? name) { name = null; if (!unicodePoint.StartsWith("[#") || !unicodePoint.EndsWith(";]")) return false; -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER if (!int.TryParse(unicodePoint[2..^2], out int codePoint)) return false; -#else - if (!int.TryParse(unicodePoint.Substring(2, unicodePoint.Length - 4), out int codePoint)) return false; -#endif name = char.ConvertFromUtf32(codePoint); return true; } @@ -1806,7 +1803,7 @@ internal bool TryParseAsUnicodePoint(string unicodePoint, out string name) [":wales:"] = "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f" }; - private static IReadOnlyCollection _unicodes; + private static IReadOnlyCollection? _unicodes; private static IReadOnlyCollection Unicodes { @@ -1817,7 +1814,7 @@ private static IReadOnlyCollection Unicodes } } - private static IReadOnlyDictionary> _unicodesAndNames; + private static IReadOnlyDictionary>? _unicodesAndNames; private static IReadOnlyDictionary> UnicodesAndNames { diff --git a/src/Kook.Net.Core/Entities/Emotes/Emote.cs b/src/Kook.Net.Core/Entities/Emotes/Emote.cs index b6438e96..50dba255 100644 --- a/src/Kook.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Kook.Net.Core/Entities/Emotes/Emote.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace Kook; @@ -38,37 +39,14 @@ internal Emote(string id, string name, bool? animated = null) } /// - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { if (obj == null) return false; - if (obj == this) return true; - if (obj is not Emote otherEmote) return false; - return Id == otherEmote.Id; } - /// - public override int GetHashCode() - => Id.GetHashCode(); - - /// Parses an from its raw format. - /// - /// The raw encoding of an emote; for example, - /// [:emotename:1991895624896587/hbCFVWhu923k03k] when is TagMode.PlainText, - /// or (emj)emotename(emj)[1991895624896587/hbCFVWhu923k03k] when is TagMode.KMarkdown. - /// - /// - /// An emote. - /// Invalid emote format. - public static Emote Parse(string text, TagMode tagMode) - { - if (TryParse(text, out Emote result, tagMode)) return result; - - throw new ArgumentException("Invalid emote format.", nameof(text)); - } - /// Tries to parse an from its raw format. /// /// The raw encoding of an emote; for example, @@ -77,28 +55,43 @@ public static Emote Parse(string text, TagMode tagMode) /// /// An emote. /// - public static bool TryParse(string text, out Emote result, TagMode tagMode) + public static bool TryParse([NotNullWhen(true)] string? text, + [NotNullWhen(true)] out Emote? result, TagMode tagMode) { result = null; - - if (text == null) return false; - + if (text == null) + return false; Match match = tagMode switch { TagMode.PlainText => PlainTextEmojiRegex.Match(text), TagMode.KMarkdown => KMarkdownEmojiRegex.Match(text), _ => throw new ArgumentOutOfRangeException(nameof(tagMode), tagMode, null) }; + if (!match.Success) + return false; + result = new Emote(match.Groups["id"].Value, match.Groups["name"].Value); + return true; + } - if (match.Success) - { - result = new Emote(match.Groups["id"].Value, match.Groups["name"].Value); - return true; - } - - return false; + /// Parses an from its raw format. + /// + /// The raw encoding of an emote; for example, + /// [:emotename:1991895624896587/hbCFVWhu923k03k] when is TagMode.PlainText, + /// or (emj)emotename(emj)[1991895624896587/hbCFVWhu923k03k] when is TagMode.KMarkdown. + /// + /// + /// An emote. + /// Invalid emote format. + public static Emote Parse([NotNull] string? text, TagMode tagMode) + { + if (TryParse(text, out Emote? result, tagMode)) + return result; + throw new ArgumentException("Invalid emote format.", nameof(text)); } + /// + public override int GetHashCode() => Id.GetHashCode(); + /// /// Gets a string representation of the emote in KMarkdown format. /// diff --git a/src/Kook.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Kook.Net.Core/Entities/Emotes/GuildEmote.cs index 91862ea5..c69e42b1 100644 --- a/src/Kook.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Kook.Net.Core/Entities/Emotes/GuildEmote.cs @@ -5,7 +5,7 @@ namespace Kook; /// /// An image-based emote that is attached to a guild. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class GuildEmote : Emote { internal GuildEmote(string id, string name, bool? animated, ulong guildId, ulong? creatorId) @@ -42,5 +42,5 @@ internal GuildEmote(string id, string name, bool? animated, ulong guildId, ulong /// public override string ToString() => $"(emj){Name}(emj)[{Id}]"; - internal GuildEmote Clone() => MemberwiseClone() as GuildEmote; + internal GuildEmote Clone() => (GuildEmote) MemberwiseClone(); } diff --git a/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs b/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs index d75b52de..07792028 100644 --- a/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs +++ b/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs @@ -5,8 +5,11 @@ namespace Kook; /// /// Represents a collection of features of a guild. /// -public class GuildFeatures +public readonly struct GuildFeatures { + /// Gets a blank that grants no permissions. + public static readonly GuildPermissions None = new(); + /// /// Gets the flags of recognized features for this guild. /// @@ -21,17 +24,17 @@ public class GuildFeatures public IReadOnlyCollection RawValues { get; } /// - /// Gets whether or not the guild is an official KOOK guild. + /// Gets whether the guild is an official KOOK guild. /// public bool IsOfficial => HasFeature(GuildFeature.Official); /// - /// Gets whether or not the guild is a partner guild. + /// Gets whether the guild is a partner guild. /// public bool IsPartner => HasFeature(GuildFeature.Partner); /// - /// Gets whether or not the guild is a key account guild. + /// Gets whether the guild is a key account guild. /// public bool IsKeyAccount => HasFeature(GuildFeature.KeyAccount); @@ -46,26 +49,23 @@ internal GuildFeatures(GuildFeature value, IEnumerable rawValues) /// /// The feature(s) to check for. /// true if this guild has the provided feature(s), otherwise false. - public bool HasFeature(GuildFeature feature) - => Value.HasFlag(feature); + public bool HasFeature(GuildFeature feature) => + Value.HasFlag(feature); /// /// Returns whether or not this guild has a feature. /// /// The feature to check for. /// true if this guild has the provided feature, otherwise false. - public bool HasFeature(string feature) - => RawValues.Contains(feature); + public bool HasFeature(string feature) => + RawValues.Contains(feature); internal void EnsureFeature(GuildFeature feature) { - if (!HasFeature(feature)) - { - IEnumerable values = Enum.GetValues(typeof(GuildFeature)).Cast(); - - IEnumerable missingValues = values.Where(x => feature.HasFlag(x) && !Value.HasFlag(x)).ToList(); - - throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); - } + if (HasFeature(feature)) return; + GuildFeatures features = this; + IEnumerable values = Enum.GetValues(typeof(GuildFeature)).Cast(); + IEnumerable missingValues = values.Where(x => feature.HasFlag(x) && !features.Value.HasFlag(x)).ToList(); + throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); } } diff --git a/src/Kook.Net.Core/Entities/Guilds/IGuild.cs b/src/Kook.Net.Core/Entities/Guilds/IGuild.cs index 62642025..d4f47ec2 100644 --- a/src/Kook.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Kook.Net.Core/Entities/Guilds/IGuild.cs @@ -72,7 +72,7 @@ public interface IGuild : IEntity /// /// An currently associated with this guild. /// - IAudioClient AudioClient { get; } + IAudioClient? AudioClient { get; } /// /// Gets whether this guild is public. @@ -198,7 +198,7 @@ public interface IGuild : IEntity /// A recommendation object that represents the recommendation information for this guild; /// null if the guild does not have a recommendation. /// - IRecommendInfo RecommendInfo { get; } + IRecommendInfo? RecommendInfo { get; } #endregion @@ -217,7 +217,7 @@ public interface IGuild : IEntity /// /// A task that represents the asynchronous leave operation. /// - Task LeaveAsync(RequestOptions options = null); + Task LeaveAsync(RequestOptions? options = null); /// /// Gets all subscriptions for this guild. @@ -227,7 +227,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous retrieval operation. The task result contains /// a collection of , each representing the subscriptions information. /// - Task>> GetBoostSubscriptionsAsync(RequestOptions options = null); + Task>> GetBoostSubscriptionsAsync(RequestOptions? options = null); /// /// Gets subscriptions which are not expired for this guild. @@ -238,7 +238,7 @@ public interface IGuild : IEntity /// a collection of which are not expired, /// each representing the subscriptions information. /// - Task>> GetActiveBoostSubscriptionsAsync(RequestOptions options = null); + Task>> GetActiveBoostSubscriptionsAsync(RequestOptions? options = null); #endregion @@ -253,7 +253,7 @@ public interface IGuild : IEntity /// ban objects that this guild currently possesses, with each object containing the user banned and reason /// behind the ban. /// - Task> GetBansAsync(RequestOptions options = null); + Task> GetBansAsync(RequestOptions? options = null); /// /// Gets a ban object for a banned user. @@ -264,7 +264,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; null if the ban entry cannot be found. /// - Task GetBanAsync(IUser user, RequestOptions options = null); + Task GetBanAsync(IUser user, RequestOptions? options = null); /// /// Gets a ban object for a banned user. @@ -275,7 +275,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; null if the ban entry cannot be found. /// - Task GetBanAsync(ulong userId, RequestOptions options = null); + Task GetBanAsync(ulong userId, RequestOptions? options = null); /// /// Bans the user from this guild and optionally prunes their recent messages. @@ -288,7 +288,7 @@ public interface IGuild : IEntity /// /// A task that represents the asynchronous add operation for the ban. /// - Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); + Task AddBanAsync(IUser user, int pruneDays = 0, string? reason = null, RequestOptions? options = null); /// /// Bans the user from this guild and optionally prunes their recent messages. @@ -301,7 +301,7 @@ public interface IGuild : IEntity /// /// A task that represents the asynchronous add operation for the ban. /// - Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); + Task AddBanAsync(ulong userId, int pruneDays = 0, string? reason = null, RequestOptions? options = null); /// /// Unbans the user if they are currently banned. @@ -311,7 +311,7 @@ public interface IGuild : IEntity /// /// A task that represents the asynchronous removal operation for the ban. /// - Task RemoveBanAsync(IUser user, RequestOptions options = null); + Task RemoveBanAsync(IUser user, RequestOptions? options = null); /// /// Unbans the user if they are currently banned. @@ -321,7 +321,7 @@ public interface IGuild : IEntity /// /// A task that represents the asynchronous removal operation for the ban. /// - Task RemoveBanAsync(ulong userId, RequestOptions options = null); + Task RemoveBanAsync(ulong userId, RequestOptions? options = null); #endregion @@ -336,7 +336,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// generic channels found within this guild. /// - Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a channel in this guild. @@ -348,7 +348,8 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains the generic channel /// associated with the specified ; null if none is found. /// - Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null); /// /// Gets a collection of all text channels in this guild. @@ -359,7 +360,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// message channels found within this guild. /// - Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a text channel in this guild. @@ -371,7 +372,8 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains the text channel /// associated with the specified ; null if none is found. /// - Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null); /// /// Gets a collection of all voice channels in this guild. @@ -382,7 +384,7 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// voice channels found within this guild. /// - Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a voice channel in this guild. @@ -394,7 +396,8 @@ public interface IGuild : IEntity /// A task that represents the asynchronous get operation. The task result contains the voice channel associated /// with the specified ; null if none is found. /// - Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null); /// /// Gets a collection of all category channels in this guild. @@ -406,7 +409,7 @@ public interface IGuild : IEntity /// category channels found within this guild. /// Task> GetCategoryChannelsAsync(CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null); + RequestOptions? options = null); /// /// Gets the default text channel for this guild. @@ -417,7 +420,7 @@ Task> GetCategoryChannelsAsync(CacheMode m /// A task that represents the asynchronous get operation. The task result contains the default text channel for this guild; /// null if none is found. /// - Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets the welcome text channel for this guild. @@ -428,7 +431,7 @@ Task> GetCategoryChannelsAsync(CacheMode m /// A task that represents the asynchronous get operation. The task result contains the welcome text channel for this guild; /// null if none is found. /// - Task GetWelcomeChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetWelcomeChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Creates a new text channel in this guild. @@ -440,7 +443,7 @@ Task> GetCategoryChannelsAsync(CacheMode m /// A task that represents the asynchronous creation operation. The task result contains the newly created /// text channel. /// - Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null); + Task CreateTextChannelAsync(string name, Action? func = null, RequestOptions? options = null); /// /// Creates a new voice channel in this guild. @@ -452,7 +455,7 @@ Task> GetCategoryChannelsAsync(CacheMode m /// A task that represents the asynchronous creation operation. The task result contains the newly created /// voice channel. /// - Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null); + Task CreateVoiceChannelAsync(string name, Action? func = null, RequestOptions? options = null); /// /// Creates a new channel category in this guild. @@ -464,8 +467,7 @@ Task> GetCategoryChannelsAsync(CacheMode m /// A task that represents the asynchronous creation operation. The task result contains the newly created /// category channel. /// - Task CreateCategoryChannelAsync(string name, Action func = null, - RequestOptions options = null); + Task CreateCategoryChannelAsync(string name, Action? func = null, RequestOptions? options = null); #endregion @@ -479,7 +481,7 @@ Task CreateCategoryChannelAsync(string name, Action - Task> GetInvitesAsync(RequestOptions options = null); + Task> GetInvitesAsync(RequestOptions? options = null); /// /// Creates a new invite to this channel. @@ -491,8 +493,7 @@ Task CreateCategoryChannelAsync(string name, Action - Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null); + Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null); /// /// Creates a new invite to this channel. @@ -504,7 +505,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// A task that represents the asynchronous invite creation operation. The task result contains an invite /// metadata object containing information for the created invite. /// - Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null); + Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions? options = null); #endregion @@ -517,7 +518,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A role that is associated with the specified ; null if none is found. /// - IRole GetRole(uint id); + IRole? GetRole(uint id); /// /// Creates a new role with the provided name. @@ -527,7 +528,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A task that represents the asynchronous creation operation. The task result contains the newly created role. /// - Task CreateRoleAsync(string name, RequestOptions options = null); + Task CreateRoleAsync(string name, RequestOptions? options = null); #endregion @@ -545,7 +546,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users found within this guild. /// - Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a user from this guild. @@ -564,7 +565,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// A task that represents the asynchronous get operation. The task result contains the guild user /// associated with the specified ; null if none is found. /// - Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets the current user for this guild. @@ -575,7 +576,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// A task that represents the asynchronous get operation. The task result contains the currently logged-in /// user within this guild. /// - Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets the owner of this guild. @@ -585,7 +586,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. /// - Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Downloads all users for this guild if the current list is incomplete. @@ -597,7 +598,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A task that represents the asynchronous download operation. /// - Task DownloadUsersAsync(RequestOptions options = null); + Task DownloadUsersAsync(RequestOptions? options = null); /// /// Downloads all voice states for this guild. @@ -609,7 +610,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A task that represents the asynchronous download operation. /// - Task DownloadVoiceStatesAsync(RequestOptions options = null); + Task DownloadVoiceStatesAsync(RequestOptions? options = null); /// /// Downloads all boost subscriptions for this guild. @@ -623,7 +624,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// /// A task that represents the asynchronous download operation. /// - Task DownloadBoostSubscriptionsAsync(RequestOptions options = null); + Task DownloadBoostSubscriptionsAsync(RequestOptions? options = null); /// /// Gets a collection of users in this guild that the name or nickname contains the @@ -641,7 +642,7 @@ Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, Invi /// users that matches the properties with the provided at . /// IAsyncEnumerable> SearchUsersAsync(Action func, - int limit = KookConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + int limit = KookConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -655,7 +656,7 @@ IAsyncEnumerable> SearchUsersAsync(Action - Task> GetEmotesAsync(RequestOptions options = null); + Task> GetEmotesAsync(RequestOptions? options = null); /// /// Gets a specific emote from this guild. @@ -666,7 +667,7 @@ IAsyncEnumerable> SearchUsersAsync(Action; null if none is found. /// - Task GetEmoteAsync(string id, RequestOptions options = null); + Task GetEmoteAsync(string id, RequestOptions? options = null); /// /// Creates a new in this guild. @@ -677,7 +678,7 @@ IAsyncEnumerable> SearchUsersAsync(Action /// A task that represents the asynchronous creation operation. The task result contains the created emote. /// - Task CreateEmoteAsync(string name, Image image, RequestOptions options = null); + Task CreateEmoteAsync(string name, Image image, RequestOptions? options = null); /// /// Modifies an existing in this guild. @@ -689,7 +690,7 @@ IAsyncEnumerable> SearchUsersAsync(Action - Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions options = null); + Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions? options = null); /// /// Deletes an existing from this guild. @@ -699,7 +700,7 @@ IAsyncEnumerable> SearchUsersAsync(Action /// A task that represents the asynchronous removal operation. /// - Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); + Task DeleteEmoteAsync(GuildEmote emote, RequestOptions? options = null); #endregion @@ -712,7 +713,7 @@ IAsyncEnumerable> SearchUsersAsync(Actionthe channel where the user gets moved to. /// The options to be used when sending the request. /// A task that represents the asynchronous operation for moving a user. - Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, RequestOptions options = null); + Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, RequestOptions? options = null); #endregion @@ -727,7 +728,7 @@ IAsyncEnumerable> SearchUsersAsync(Action - Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions options = null); + Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Guilds/IRecommendInfo.cs b/src/Kook.Net.Core/Entities/Guilds/IRecommendInfo.cs index 69289475..7a6a5f72 100644 --- a/src/Kook.Net.Core/Entities/Guilds/IRecommendInfo.cs +++ b/src/Kook.Net.Core/Entities/Guilds/IRecommendInfo.cs @@ -16,7 +16,7 @@ public interface IRecommendInfo /// /// Gets the open ID for the recommended guild. /// - uint OpenId { get; } + uint? OpenId { get; } /// /// Gets the default channel ID of the recommended guild. diff --git a/src/Kook.Net.Core/Entities/Guilds/RoleType.cs b/src/Kook.Net.Core/Entities/Guilds/RoleType.cs index 4dcf0880..4a73589e 100644 --- a/src/Kook.Net.Core/Entities/Guilds/RoleType.cs +++ b/src/Kook.Net.Core/Entities/Guilds/RoleType.cs @@ -21,7 +21,7 @@ public enum RoleType : ushort Booster = 2, /// - /// Represents tht role is the default everyone role. + /// Represents the role is the default everyone role. /// Everyone = 255 } diff --git a/src/Kook.Net.Core/Entities/IDeletable.cs b/src/Kook.Net.Core/Entities/IDeletable.cs index 5caddd73..7ba19a94 100644 --- a/src/Kook.Net.Core/Entities/IDeletable.cs +++ b/src/Kook.Net.Core/Entities/IDeletable.cs @@ -9,5 +9,5 @@ public interface IDeletable /// Deletes this object and all its children. /// /// The options to be used when sending the request. - Task DeleteAsync(RequestOptions options = null); + Task DeleteAsync(RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/IUpdateable.cs b/src/Kook.Net.Core/Entities/IUpdateable.cs index 76c72508..a204b9e7 100644 --- a/src/Kook.Net.Core/Entities/IUpdateable.cs +++ b/src/Kook.Net.Core/Entities/IUpdateable.cs @@ -18,5 +18,5 @@ public interface IUpdateable /// and replace the current object's properties with the new data. /// /// - Task UpdateAsync(RequestOptions options = null); + Task UpdateAsync(RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Image.cs b/src/Kook.Net.Core/Entities/Image.cs index c8125e99..0793c7bc 100644 --- a/src/Kook.Net.Core/Entities/Image.cs +++ b/src/Kook.Net.Core/Entities/Image.cs @@ -15,7 +15,7 @@ public struct Image : IDisposable /// /// Gets the file extension of the image if possible. /// - internal string FileExtension { get; } + internal string? FileExtension { get; } /// /// Create the image with a . @@ -28,7 +28,8 @@ public Image(Stream stream) { _isDisposed = false; Stream = stream; - if (stream is FileStream fileStream) FileExtension = Path.GetExtension(fileStream.Name).Replace(".", ""); + if (stream is FileStream fileStream) + FileExtension = Path.GetExtension(fileStream.Name).Replace(".", ""); } internal Image(Stream stream, string fileExtension) @@ -76,7 +77,8 @@ public Image(string path) /// public void Dispose() { - if (_isDisposed) return; + if (_isDisposed) + return; Stream?.Dispose(); _isDisposed = true; diff --git a/src/Kook.Net.Core/Entities/Intimacies/IIntimacy.cs b/src/Kook.Net.Core/Entities/Intimacies/IIntimacy.cs index 2c69a917..3d8b9a67 100644 --- a/src/Kook.Net.Core/Entities/Intimacies/IIntimacy.cs +++ b/src/Kook.Net.Core/Entities/Intimacies/IIntimacy.cs @@ -35,7 +35,7 @@ public interface IIntimacy : IEntity /// /// A time at which this intimacy was modified last time. /// - DateTimeOffset LastModifyAt { get; } + DateTimeOffset? LastModifyAt { get; } /// /// Gets the score associated with this intimacy. @@ -59,5 +59,5 @@ public interface IIntimacy : IEntity /// A delegate containing the properties to modify the with. /// The options to be used when sending the request. /// A task that represents the asynchronous operation for updating the intimacy information. - Task UpdateAsync(Action func, RequestOptions options); + Task UpdateAsync(Action func, RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Intimacies/IntimacyProperties.cs b/src/Kook.Net.Core/Entities/Intimacies/IntimacyProperties.cs index 6c2db164..2e3f215a 100644 --- a/src/Kook.Net.Core/Entities/Intimacies/IntimacyProperties.cs +++ b/src/Kook.Net.Core/Entities/Intimacies/IntimacyProperties.cs @@ -6,18 +6,24 @@ namespace Kook; /// public class IntimacyProperties { + internal IntimacyProperties(string socialInfo, int score) + { + SocialInfo = socialInfo; + Score = score; + } + /// /// The social information to be set on the . /// public string SocialInfo { get; set; } /// - /// The ID of the image to be updated on the . + /// The score to be set on the . /// - public uint ImageId { get; set; } + public int Score { get; set; } /// - /// The score to be set on the . + /// The ID of the image to be updated on the . /// - public int Score { get; set; } + public uint? ImageId { get; set; } } diff --git a/src/Kook.Net.Core/Entities/Invites/IInvite.cs b/src/Kook.Net.Core/Entities/Invites/IInvite.cs index e1c909b7..ffe016a6 100644 --- a/src/Kook.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Kook.Net.Core/Entities/Invites/IInvite.cs @@ -56,7 +56,7 @@ public interface IInvite : IEntity, IDeletable /// /// A string containing the name of the channel that the invite points to. /// - string ChannelName { get; } + string? ChannelName { get; } /// /// Gets the guild this invite is linked to. @@ -82,6 +82,14 @@ public interface IInvite : IEntity, IDeletable /// string GuildName { get; } + /// + /// Gets the time at which this invite was created. + /// + /// + /// A representing the time at which this invite was created. + /// + DateTimeOffset CreatedAt { get; } + /// /// Gets the time at which this invite will expire. /// @@ -124,4 +132,12 @@ public interface IInvite : IEntity, IDeletable /// An int representing the number of times this invite still remains; null if none is set. /// int? RemainingUses { get; } + + /// + /// Gets the number of users that have accepted this invite. + /// + /// + /// An int representing the number of users that have accepted this invite. + /// + int InvitedUsersCount { get; } } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Card.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Card.cs index 8b6b8640..4f57f86b 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Card.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Card.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,7 +8,7 @@ namespace Kook; /// Represents a card object seen in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class Card : ICard, IEquatable +public class Card : ICard, IEquatable, IEquatable { internal Card(CardTheme theme, CardSize size, Color? color, ImmutableArray modules) { @@ -61,28 +62,28 @@ internal Card(CardTheme theme, CardSize size, Color? color, ImmutableArray is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(Card left, Card right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(Card left, Card right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(Card left, Card right) - => !(left == right); + public static bool operator !=(Card left, Card right) => + !(left == right); /// Determines whether the specified object is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is Card card && Equals(card); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is Card card && Equals(card); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(Card card) - => GetHashCode() == card?.GetHashCode(); + public bool Equals([NotNullWhen(true)] Card? card) => + GetHashCode() == card?.GetHashCode(); /// public override int GetHashCode() @@ -91,9 +92,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ (Type, Theme, Color, Size).GetHashCode(); - foreach (IModule module in Modules) hash = (hash * 16777619) ^ module.GetHashCode(); - + foreach (IModule module in Modules) + hash = (hash * 16777619) ^ module.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] ICard? card) => + Equals(card as Card); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/CardBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/CardBuilder.cs index 0958c63d..19ffe5c8 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/CardBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/CardBuilder.cs @@ -1,17 +1,12 @@ -using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Represents a builder class for creating a . /// -public class CardBuilder : ICardBuilder, IEquatable +public class CardBuilder : ICardBuilder, IEquatable, IEquatable { - /// - /// Initializes a new instance of the class. - /// - public CardBuilder() => Modules = new List(); - /// /// Initializes a new instance of the class with the specified parameters. /// @@ -19,12 +14,12 @@ public class CardBuilder : ICardBuilder, IEquatable /// The color displayed along the left side of the card. /// The size of the card. /// The modules in the card. - public CardBuilder(CardTheme theme, Color? color = null, CardSize size = CardSize.Large, List modules = null) + public CardBuilder(CardTheme theme = CardTheme.Primary, Color? color = null, CardSize size = CardSize.Large, IList? modules = null) { - WithTheme(theme); - if (color.HasValue) WithColor(color.Value); - WithSize(size); - Modules = modules ?? new List(); + Theme = theme; + Color = color; + Size = size; + Modules = modules ?? []; } /// @@ -57,15 +52,15 @@ public CardBuilder(CardTheme theme, Color? color = null, CardSize size = CardSiz /// /// A value that represents the size of the card. /// - public CardSize Size { get; set; } + public CardSize Size { get; set; } = CardSize.Large; /// /// Gets or sets the modules in the card. /// /// - /// A containing the modules in the card. + /// An containing the modules in the card. /// - public List Modules { get; set; } + public IList Modules { get; set; } /// /// Sets the theme of the card. @@ -91,7 +86,7 @@ public CardBuilder WithTheme(CardTheme theme) /// /// The current builder. /// - public CardBuilder WithColor(Color color) + public CardBuilder WithColor(Color? color) { Color = color; return this; @@ -136,7 +131,7 @@ public CardBuilder AddModule(IModuleBuilder module) /// /// The current builder. /// - public CardBuilder AddModule(Action action = null) + public CardBuilder AddModule(Action? action = null) where T : IModuleBuilder, new() { T module = new(); @@ -152,7 +147,7 @@ public CardBuilder AddModule(Action action = null) /// A represents the built element object. /// public Card Build() => - new(Theme, Size, Color, Modules.Select(m => m.Build()).ToImmutableArray()); + new(Theme, Size, Color, [..Modules.Select(m => m.Build())]); /// ICard ICardBuilder.Build() => Build(); @@ -163,8 +158,8 @@ public Card Build() => /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(CardBuilder left, CardBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(CardBuilder? left, CardBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -172,29 +167,32 @@ public Card Build() => /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(CardBuilder left, CardBuilder right) - => !(left == right); + public static bool operator !=(CardBuilder? left, CardBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The object to compare with the current . /// true if the specified object is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is CardBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is CardBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(CardBuilder cardBuilder) + public bool Equals([NotNullWhen(true)] CardBuilder? cardBuilder) { - if (cardBuilder is null) return false; + if (cardBuilder is null) + return false; - if (Modules.Count != cardBuilder.Modules.Count) return false; + if (Modules.Count != cardBuilder.Modules.Count) + return false; - for (int i = 0; i < Modules.Count; i++) - if (Modules[i] != cardBuilder.Modules[i]) - return false; + if (Modules + .Zip(cardBuilder.Modules, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == cardBuilder.Type && Theme == cardBuilder.Theme @@ -204,4 +202,7 @@ public bool Equals(CardBuilder cardBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] ICardBuilder? cardBuilder) => + Equals(cardBuilder as CardBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ButtonElementBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ButtonElementBuilder.cs index dbf1a572..7062b7a3 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ButtonElementBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ButtonElementBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Kook.Utils; namespace Kook; @@ -5,10 +6,8 @@ namespace Kook; /// /// An element builder to build a . /// -public class ButtonElementBuilder : IElementBuilder, IEquatable +public class ButtonElementBuilder : IElementBuilder, IEquatable, IEquatable { - private IElementBuilder _text; - /// /// Gets the maximum button text length allowed by Kook. /// @@ -29,13 +28,12 @@ public ButtonElementBuilder() /// The value of the button. /// The type of the click event. public ButtonElementBuilder(string text, ButtonTheme theme = ButtonTheme.Primary, - string value = null, - ButtonClickEventType click = ButtonClickEventType.None) + string? value = null, ButtonClickEventType click = ButtonClickEventType.None) { - WithText(text); - WithTheme(theme); - WithValue(value); - WithClick(click); + Text = new PlainTextElementBuilder(text); + Theme = theme; + Value = value; + Click = click; } /// @@ -64,7 +62,7 @@ public ButtonElementBuilder(string text, ButtonTheme theme = ButtonTheme.Primary /// If the is set to , /// the value of the property will be returned when the button is clicked. /// - public string Value { get; set; } + public string? Value { get; set; } /// /// Gets or sets the type of the click event. @@ -80,38 +78,10 @@ public ButtonElementBuilder(string text, ButtonTheme theme = ButtonTheme.Primary /// /// An that represents the text of the button. /// - /// - /// The is neither a nor a . - /// /// /// This property only takes a or a . /// - /// - /// The length of is greater than . - /// - public IElementBuilder Text - { - get => _text; - set - { - string text = value switch - { - PlainTextElementBuilder plainText => plainText.Content, - KMarkdownElementBuilder kMarkdown => kMarkdown.Content, - _ => throw new ArgumentException( - $"The text of a button must be a {nameof(PlainTextElementBuilder)} or a {nameof(KMarkdownElementBuilder)}.", - nameof(value)) - }; - if (string.IsNullOrEmpty(text)) throw new ArgumentException("The content cannot be null or empty.", nameof(value)); - - if (text.Length > MaxButtonTextLength) - throw new ArgumentException( - $"The length of button text must be less than or equal to {MaxButtonTextLength}.", - nameof(value)); - - _text = value; - } - } + public IElementBuilder? Text { get; set; } /// /// Sets the theme of a . @@ -133,7 +103,7 @@ public ButtonElementBuilder WithTheme(ButtonTheme theme) /// /// The current builder. /// - public ButtonElementBuilder WithValue(string value) + public ButtonElementBuilder WithValue(string? value) { Value = value; return this; @@ -195,11 +165,11 @@ public ButtonElementBuilder WithText(KMarkdownElementBuilder text) /// /// The current builder. /// - public ButtonElementBuilder WithText(Action action = null) + public ButtonElementBuilder WithText(Action action) where T : IElementBuilder, new() { T text = new(); - action?.Invoke(text); + action.Invoke(text); Text = text; return this; } @@ -222,8 +192,8 @@ public ButtonElementBuilder WithText(string text, bool isKMarkdown = false) { Text = isKMarkdown switch { - false => new PlainTextElementBuilder().WithContent(text), - true => new KMarkdownElementBuilder().WithContent(text) + false => new PlainTextElementBuilder(text), + true => new KMarkdownElementBuilder(text) }; return this; } @@ -234,45 +204,89 @@ public ButtonElementBuilder WithText(string text, bool isKMarkdown = false) /// /// A represents the built element object. /// + /// + /// The is neither a nor a . + /// + /// + /// The is null. + /// + /// + /// The is empty. + /// + /// + /// The length of is greater than . + /// + /// + /// The of a button with a link event type is null or empty. + /// + [MemberNotNull(nameof(Text))] public ButtonElement Build() { - if (Click == ButtonClickEventType.Link && !UrlValidation.Validate(Value)) - throw new ArgumentException("The value of a button with a link event type cannot be null or empty.", nameof(Value)); + string? text = Text switch + { + PlainTextElementBuilder plainText => plainText.Content, + KMarkdownElementBuilder kMarkdown => kMarkdown.Content, + _ => throw new ArgumentException( + $"The text of a button must be a {nameof(PlainTextElementBuilder)} or a {nameof(KMarkdownElementBuilder)}.", + nameof(Text)) + }; + + if (text is null) + throw new ArgumentNullException(nameof(Text), "The text of a button cannot be null."); + + if (string.IsNullOrEmpty(text)) + throw new ArgumentException("The text of a button cannot be empty.", nameof(Text)); - return new ButtonElement(Theme, Value, Click, Text?.Build()); + if (text.Length > MaxButtonTextLength) + { + throw new ArgumentException( + $"The length of button text must be less than or equal to {MaxButtonTextLength}.", + nameof(Text)); + } + + if (Click == ButtonClickEventType.Link) + { + if (Value is null || string.IsNullOrEmpty(Value)) + throw new ArgumentException("The value of a button with a link event type cannot be null or empty.", nameof(Value)); + UrlValidation.Validate(Value); + } + + return new ButtonElement(Theme, Value, Click, Text.Build()); } /// + [MemberNotNull(nameof(Text))] IElement IElementBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ButtonElementBuilder left, ButtonElementBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ButtonElementBuilder? left, ButtonElementBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ButtonElementBuilder left, ButtonElementBuilder right) - => !(left == right); + public static bool operator !=(ButtonElementBuilder? left, ButtonElementBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ButtonElementBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ButtonElementBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ButtonElementBuilder buttonElementBuilder) + public bool Equals([NotNullWhen(true)] ButtonElementBuilder? buttonElementBuilder) { - if (buttonElementBuilder is null) return false; + if (buttonElementBuilder is null) + return false; return Type == buttonElementBuilder.Type && Theme == buttonElementBuilder.Theme @@ -283,4 +297,7 @@ public bool Equals(ButtonElementBuilder buttonElementBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElementBuilder? elementBuilder) => + Equals(elementBuilder as ButtonElementBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ImageElementBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ImageElementBuilder.cs index d8029a60..7e5697c8 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ImageElementBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ImageElementBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Kook.Utils; namespace Kook; @@ -5,10 +6,8 @@ namespace Kook; /// /// An element builder to build an . /// -public class ImageElementBuilder : IElementBuilder, IEquatable +public class ImageElementBuilder : IElementBuilder, IEquatable, IEquatable { - private string _alternative; - /// /// Gets the maximum image alternative text length allowed by Kook. /// @@ -28,12 +27,13 @@ public ImageElementBuilder() /// The alternative text of the image. /// The size of the image. /// Whether the image should be rendered as a circle. - public ImageElementBuilder(string source, string alternative = null, ImageSize? size = null, bool? circle = null) + public ImageElementBuilder(string source, string? alternative = null, ImageSize size = ImageSize.Small, + bool circle = false) { - WithSource(source); - WithAlternative(alternative); - WithSize(size ?? ImageSize.Small); - WithCircle(circle ?? false); + Source = source; + Alternative = alternative; + Size = size; + Circle = circle; } /// @@ -50,30 +50,15 @@ public ImageElementBuilder(string source, string alternative = null, ImageSize? /// /// A string that represents the source of the . /// - public string Source { get; set; } + public string? Source { get; set; } /// /// Gets or sets the alternative text of an . /// - /// - /// The length of is greater than . - /// /// /// A string that represents the alternative text of the . /// - public string Alternative - { - get => _alternative; - set - { - if (value?.Length > MaxAlternativeLength) - throw new ArgumentException( - $"Image alternative length must be less than or equal to {MaxAlternativeLength}.", - nameof(Alternative)); - - _alternative = value; - } - } + public string? Alternative { get; set; } /// /// Gets or sets the size of the image of an . @@ -102,7 +87,7 @@ public string Alternative /// /// The current builder. /// - public ImageElementBuilder WithSource(string source) + public ImageElementBuilder WithSource(string? source) { Source = source; return this; @@ -117,10 +102,7 @@ public ImageElementBuilder WithSource(string source) /// /// The current builder. /// - /// - /// The length of is greater than . - /// - public ImageElementBuilder WithAlternative(string alternative) + public ImageElementBuilder WithAlternative(string? alternative) { Alternative = alternative; return this; @@ -135,7 +117,7 @@ public ImageElementBuilder WithAlternative(string alternative) /// /// The current builder. /// - public ImageElementBuilder WithSize(ImageSize size) + public ImageElementBuilder WithSize(ImageSize? size) { Size = size; return this; @@ -150,7 +132,7 @@ public ImageElementBuilder WithSize(ImageSize size) /// /// The current builder. /// - public ImageElementBuilder WithCircle(bool circle) + public ImageElementBuilder WithCircle(bool? circle) { Circle = circle; return this; @@ -162,12 +144,34 @@ public ImageElementBuilder WithCircle(bool circle) /// /// An represents the built element object. /// + /// + /// The url is null. + /// + /// + /// The url is empty. + /// /// - /// The source url does not include a protocol (either HTTP or HTTPS). + /// The url does not include a protocol (either HTTP or HTTPS). /// + /// + /// The length of is greater than . + /// + [MemberNotNull(nameof(Source))] public ImageElement Build() { - if (!string.IsNullOrEmpty(Source)) UrlValidation.Validate(Source); + if (Source == null) + throw new ArgumentNullException(nameof(Source), "The source url cannot be null or empty."); + if (string.IsNullOrEmpty(Source)) + throw new ArgumentException("The source url cannot be null or empty.", nameof(Source)); + + UrlValidation.Validate(Source); + + if (Alternative?.Length > MaxAlternativeLength) + { + throw new ArgumentException( + $"Image alternative length must be less than or equal to {MaxAlternativeLength}.", + nameof(Alternative)); + } return new ImageElement(Source, Alternative, Size, Circle); } @@ -182,41 +186,38 @@ public ImageElement Build() /// /// An object that is initialized with the specified image source. /// - /// - /// The length of is greater than . - /// - public static implicit operator ImageElementBuilder(string source) => new ImageElementBuilder() - .WithSource(source); + public static implicit operator ImageElementBuilder(string source) => new(source); /// + [MemberNotNull(nameof(Source))] IElement IElementBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ImageElementBuilder left, ImageElementBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ImageElementBuilder? left, ImageElementBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ImageElementBuilder left, ImageElementBuilder right) - => !(left == right); + public static bool operator !=(ImageElementBuilder? left, ImageElementBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ImageElementBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ImageElementBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ImageElementBuilder imageElementBuilder) + public bool Equals([NotNullWhen(true)] ImageElementBuilder? imageElementBuilder) { if (imageElementBuilder is null) return false; @@ -229,4 +230,7 @@ public bool Equals(ImageElementBuilder imageElementBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElementBuilder? elementBuilder) => + Equals(elementBuilder as ImageElementBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/KMarkdownElementBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/KMarkdownElementBuilder.cs index 6fabebbb..cc3700fb 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/KMarkdownElementBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/KMarkdownElementBuilder.cs @@ -1,12 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// An element builder to build a . /// -public class KMarkdownElementBuilder : IElementBuilder, IEquatable +public class KMarkdownElementBuilder : IElementBuilder, IEquatable, IEquatable { - private string _content; - /// /// Gets the maximum KMarkdown length allowed by Kook. /// @@ -23,7 +23,10 @@ public KMarkdownElementBuilder() /// Initializes a new instance of the class. /// /// - public KMarkdownElementBuilder(string content) => WithContent(content); + public KMarkdownElementBuilder(string? content) + { + Content = content; + } /// /// Gets the type of the element that this builder builds. @@ -36,30 +39,10 @@ public KMarkdownElementBuilder() /// /// Gets or sets the content of a . /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// /// /// The content of the . /// - public string Content - { - get => _content; - set - { - if (value is null) throw new ArgumentException("The content cannot be null.", nameof(value)); - - if (value.Length > MaxKMarkdownLength) - throw new ArgumentException( - $"KMarkdown length must be less than or equal to {MaxKMarkdownLength}.", - nameof(Content)); - - _content = value; - } - } + public string? Content { get; set; } /// /// Sets the content of a . @@ -68,12 +51,6 @@ public string Content /// /// The current builder. /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// public KMarkdownElementBuilder WithContent(string content) { Content = content; @@ -86,8 +63,25 @@ public KMarkdownElementBuilder WithContent(string content) /// /// A represents the built element object. /// + /// + /// The is null. + /// + /// + /// The length of is greater than . + /// + [MemberNotNull(nameof(Content))] public KMarkdownElement Build() - => new(Content); + { + if (Content == null) + throw new ArgumentNullException(nameof(Content), $"The {nameof(Content)} cannot be null."); + + if (Content.Length > MaxKMarkdownLength) + throw new ArgumentException( + $"KMarkdown length must be less than or equal to {MaxKMarkdownLength}.", + nameof(Content)); + + return new KMarkdownElement(Content); + } /// /// Initialized a new instance of the class @@ -99,51 +93,48 @@ public KMarkdownElement Build() /// /// A object that is initialized with the specified content. /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// - public static implicit operator KMarkdownElementBuilder(string content) => - new KMarkdownElementBuilder().WithContent(content); + public static implicit operator KMarkdownElementBuilder(string content) => new(content); /// + [MemberNotNull(nameof(Content))] IElement IElementBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(KMarkdownElementBuilder left, KMarkdownElementBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(KMarkdownElementBuilder? left, KMarkdownElementBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(KMarkdownElementBuilder left, KMarkdownElementBuilder right) - => !(left == right); + public static bool operator !=(KMarkdownElementBuilder? left, KMarkdownElementBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is KMarkdownElementBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is KMarkdownElementBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(KMarkdownElementBuilder kMarkdownElementBuilder) + public bool Equals([NotNullWhen(true)] KMarkdownElementBuilder? kMarkdownElementBuilder) { - if (kMarkdownElementBuilder is null) return false; - + if (kMarkdownElementBuilder is null) + return false; return Type == kMarkdownElementBuilder.Type && Content == kMarkdownElementBuilder.Content; } /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElementBuilder? elementBuilder) => + Equals(elementBuilder as KMarkdownElementBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ParagraphStructBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ParagraphStructBuilder.cs index 6b58165e..280f0af0 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ParagraphStructBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/ParagraphStructBuilder.cs @@ -1,15 +1,12 @@ -using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// An element builder to build a . /// -public class ParagraphStructBuilder : IElementBuilder, IEquatable +public class ParagraphStructBuilder : IElementBuilder, IEquatable, IEquatable { - private int _columnCount; - private List _fields; - /// /// Returns the maximum number of fields allowed by Kook. /// @@ -30,16 +27,16 @@ public class ParagraphStructBuilder : IElementBuilder, IEquatable public ParagraphStructBuilder() { - Fields = new List(); + Fields = []; } /// /// Initializes a new class. /// - public ParagraphStructBuilder(int columnCount, List fields = null) + public ParagraphStructBuilder(int columnCount, IList? fields = null) { - WithColumnCount(columnCount); - Fields = fields ?? new List(); + ColumnCount = columnCount; + Fields = fields ?? []; } /// @@ -53,64 +50,18 @@ public ParagraphStructBuilder(int columnCount, List fields = nu /// /// Gets or sets the number of columns of the paragraph. /// - /// - /// The is less than or greater than . - /// /// /// An int that represents the number of columns of the paragraph. /// - public int ColumnCount - { - get => _columnCount; - set => - _columnCount = value switch - { - < MinColumnCount => throw new ArgumentException( - $"Column must be more than or equal to {MinColumnCount}.", nameof(ColumnCount)), - > MaxColumnCount => throw new ArgumentException( - $"Column must be less than or equal to {MaxColumnCount}.", nameof(ColumnCount)), - _ => value - }; - } + public int ColumnCount { get; set; } = MinColumnCount; /// /// Gets or sets the fields of the paragraph. /// - /// - /// The is null. - /// - /// - /// The contains more than elements. - /// - /// - /// The contains an element that is neither a nor a . - /// /// - /// A that represents the fields of the paragraph. + /// An that represents the fields of the paragraph. /// - public List Fields - { - get => _fields; - set - { - if (value == null) - throw new ArgumentNullException( - nameof(Fields), - "Cannot set an paragraph struct builder's fields collection to null."); - - if (value.Count > MaxFieldCount) - throw new ArgumentException( - $"Field count must be less than or equal to {MaxFieldCount}.", - nameof(Fields)); - - if (value.Any(field => field is not PlainTextElementBuilder && field is not KMarkdownElementBuilder)) - throw new ArgumentException( - "The elements of fields in a paragraph must be PlainTextElementBuilder or KMarkdownElementBuilder.", - nameof(Fields)); - - _fields = value; - } - } + public IList Fields { get; set; } /// /// Sets the number of columns of the paragraph. @@ -133,19 +84,11 @@ public ParagraphStructBuilder WithColumnCount(int count) /// /// A that represents the field to add. /// - /// - /// The addition operation will result in a field count greater than . - /// /// /// The current builder. /// public ParagraphStructBuilder AddField(PlainTextElementBuilder field) { - if (Fields.Count >= MaxFieldCount) - throw new ArgumentException( - $"Field count must be less than or equal to {MaxFieldCount}.", - nameof(field)); - Fields.Add(field); return this; } @@ -156,19 +99,11 @@ public ParagraphStructBuilder AddField(PlainTextElementBuilder field) /// /// A that represents the field to add. /// - /// - /// The addition operation will result in a field count greater than . - /// /// /// The current builder. /// public ParagraphStructBuilder AddField(KMarkdownElementBuilder field) { - if (Fields.Count >= MaxFieldCount) - throw new ArgumentException( - $"Field count must be less than or equal to {MaxFieldCount}.", - nameof(field)); - Fields.Add(field); return this; } @@ -179,31 +114,15 @@ public ParagraphStructBuilder AddField(KMarkdownElementBuilder field) /// /// The action to create a builder of a , which will be added to the paragraph. /// - /// - /// The addition operation will result in a field count greater than . - /// /// /// The current builder. /// - public ParagraphStructBuilder AddField(Action action = null) + public ParagraphStructBuilder AddField(Action? action = null) where T : IElementBuilder, new() { T field = new(); action?.Invoke(field); - switch (field) - { - case PlainTextElementBuilder plainText: - AddField(plainText); - break; - case KMarkdownElementBuilder kMarkdown: - AddField(kMarkdown); - break; - default: - throw new ArgumentException( - "The elements of fields in a paragraph must be PlainTextElementBuilder or KMarkdownElementBuilder.", - nameof(field)); - } - + Fields.Add(field); return this; } @@ -213,8 +132,59 @@ public ParagraphStructBuilder AddField(Action action = null) /// /// A represents the built element object. /// - public ParagraphStruct Build() => - new(ColumnCount, Fields.Select(f => f.Build()).ToImmutableArray()); + /// + /// The is less than . + /// + /// + /// The is greater than . + /// + /// + /// The is null. + /// + /// + /// The number of is greater than . + /// + /// + /// The contain an element that is not a + /// or . + /// + public ParagraphStruct Build() + { + if (ColumnCount < MinColumnCount) + { + throw new ArgumentOutOfRangeException( + nameof(ColumnCount), $"Column must be more than or equal to {MinColumnCount}."); + } + + if (ColumnCount > MaxColumnCount) + { + throw new ArgumentOutOfRangeException( + nameof(ColumnCount), $"Column must be less than or equal to {MaxColumnCount}."); + } + + if (Fields == null) + { + throw new ArgumentNullException( + nameof(Fields), + "The fields of a paragraph cannot be null."); + } + + if (Fields.Count > MaxFieldCount) + { + throw new ArgumentException( + $"Field count must be less than or equal to {MaxFieldCount}.", + nameof(Fields)); + } + + if (Fields.Any(field => field is not (PlainTextElementBuilder or KMarkdownElementBuilder))) + { + throw new ArgumentException( + "The elements of fields in a paragraph must be PlainTextElementBuilder or KMarkdownElementBuilder.", + nameof(Fields)); + } + + return new ParagraphStruct(ColumnCount, [..Fields.Select(f => f.Build())]); + } /// IElement IElementBuilder.Build() => Build(); @@ -223,36 +193,39 @@ public ParagraphStruct Build() => /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ParagraphStructBuilder left, ParagraphStructBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ParagraphStructBuilder? left, ParagraphStructBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ParagraphStructBuilder left, ParagraphStructBuilder right) - => !(left == right); + public static bool operator !=(ParagraphStructBuilder? left, ParagraphStructBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ParagraphStructBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ParagraphStructBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ParagraphStructBuilder paragraphStructBuilder) + public bool Equals([NotNullWhen(true)] ParagraphStructBuilder? paragraphStructBuilder) { - if (paragraphStructBuilder is null) return false; + if (paragraphStructBuilder is null) + return false; - if (Fields.Count != paragraphStructBuilder.Fields.Count) return false; + if (Fields.Count != paragraphStructBuilder.Fields.Count) + return false; - for (int i = 0; i < Fields.Count; i++) - if (Fields[i] != paragraphStructBuilder.Fields[i]) - return false; + if (Fields + .Zip(paragraphStructBuilder.Fields, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == paragraphStructBuilder.Type && ColumnCount == paragraphStructBuilder.ColumnCount; @@ -260,4 +233,7 @@ public bool Equals(ParagraphStructBuilder paragraphStructBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElementBuilder? elementBuilder) => + Equals(elementBuilder as ParagraphStructBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/PlainTextElementBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/PlainTextElementBuilder.cs index e73e0bd6..d4f198b2 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/PlainTextElementBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/Builders/PlainTextElementBuilder.cs @@ -1,12 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// An element builder to build a . /// -public class PlainTextElementBuilder : IElementBuilder, IEquatable +public class PlainTextElementBuilder : IElementBuilder, IEquatable, IEquatable { - private string _content; - /// /// Gets the maximum plain text length allowed by Kook. /// @@ -20,6 +20,7 @@ public class PlainTextElementBuilder : IElementBuilder, IEquatable public PlainTextElementBuilder() { + Content = string.Empty; } /// @@ -27,10 +28,10 @@ public PlainTextElementBuilder() /// /// The content of the . /// A boolean value that indicates whether the shortcuts should be translated into emojis. - public PlainTextElementBuilder(string content, bool emoji = true) + public PlainTextElementBuilder(string? content, bool emoji = true) { - WithContent(content); - WithEmoji(emoji); + Content = content; + Emoji = emoji; } /// @@ -47,27 +48,7 @@ public PlainTextElementBuilder(string content, bool emoji = true) /// /// The content of the . /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// - public string Content - { - get => _content; - set - { - if (value is null) throw new ArgumentException("The content cannot be null.", nameof(value)); - - if (value.Length > MaxPlainTextLength) - throw new ArgumentException( - $"Plain text length must be less than or equal to {MaxPlainTextLength}.", - nameof(Content)); - - _content = value; - } - } + public string? Content { get; set; } /// /// Gets whether the shortcuts should be translated into emojis. @@ -86,12 +67,6 @@ public string Content /// /// The current builder. /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// public PlainTextElementBuilder WithContent(string content) { Content = content; @@ -121,8 +96,25 @@ public PlainTextElementBuilder WithEmoji(bool emoji) /// /// A represents the built element object. /// + /// + /// The is null. + /// + /// + /// The length of the is greater than . + /// + [MemberNotNull(nameof(Content))] public PlainTextElement Build() - => new(Content, Emoji); + { + if (Content == null) + throw new ArgumentNullException(nameof(Content), $"The {nameof(Content)} cannot be null."); + + if (Content.Length > MaxPlainTextLength) + throw new ArgumentException( + $"Plain text length must be less than or equal to {MaxPlainTextLength}.", + nameof(Content)); + + return new PlainTextElement(Content, Emoji); + } /// /// Initialized a new instance of the class @@ -134,44 +126,38 @@ public PlainTextElement Build() /// /// A object that is initialized with the specified content. /// - /// - /// The cannot be null. - /// - /// - /// The length of is greater than . - /// - public static implicit operator PlainTextElementBuilder(string content) => - new PlainTextElementBuilder().WithContent(content); + public static implicit operator PlainTextElementBuilder(string content) => new(content); /// + [MemberNotNull(nameof(Content))] IElement IElementBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(PlainTextElementBuilder left, PlainTextElementBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(PlainTextElementBuilder? left, PlainTextElementBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(PlainTextElementBuilder left, PlainTextElementBuilder right) - => !(left == right); + public static bool operator !=(PlainTextElementBuilder? left, PlainTextElementBuilder? right) => + !(left == right); /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// - public override bool Equals(object obj) - => obj is PlainTextElementBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is PlainTextElementBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(PlainTextElementBuilder plainTextElementBuilder) + public bool Equals([NotNullWhen(true)] PlainTextElementBuilder? plainTextElementBuilder) { if (plainTextElementBuilder is null) return false; @@ -182,4 +168,7 @@ public bool Equals(PlainTextElementBuilder plainTextElementBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElementBuilder? elementBuilder) => + Equals(elementBuilder as PlainTextElementBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ButtonElement.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ButtonElement.cs index ca6da8c1..b37e5c18 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ButtonElement.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ButtonElement.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// A button element that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ButtonElement : IElement, IEquatable +public class ButtonElement : IElement, IEquatable, IEquatable { - internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType click, IElement text) + internal ButtonElement(ButtonTheme? theme, string? value, ButtonClickEventType? click, IElement text) { Theme = theme; Value = value; @@ -30,7 +31,7 @@ internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType cli /// /// A value that represents the theme of the button. /// - public ButtonTheme Theme { get; } + public ButtonTheme? Theme { get; } /// /// Gets the value of the button. @@ -38,7 +39,7 @@ internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType cli /// /// A string value that represents the value of the button. /// - public string Value { get; } + public string? Value { get; } /// /// Gets the event type fired when the button is clicked. @@ -46,7 +47,7 @@ internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType cli /// /// A value that represents the event type fired when the button is clicked. /// - public ButtonClickEventType Click { get; } + public ButtonClickEventType? Click { get; } /// /// Gets the text element of the button. @@ -64,8 +65,8 @@ internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType cli /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(ButtonElement left, ButtonElement right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ButtonElement? left, ButtonElement? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -73,31 +74,34 @@ internal ButtonElement(ButtonTheme theme, string value, ButtonClickEventType cli /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(ButtonElement left, ButtonElement right) - => !(left == right); + public static bool operator !=(ButtonElement? left, ButtonElement? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ButtonElement buttonElement && Equals(buttonElement); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ButtonElement buttonElement && Equals(buttonElement); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ButtonElement buttonElement) - => GetHashCode() == buttonElement?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ButtonElement? buttonElement) => + GetHashCode() == buttonElement?.GetHashCode(); /// public override int GetHashCode() { unchecked { - int hash = (int)2166136261; + int hash = (int) 2166136261; hash = (hash * 16777619) ^ (Type, Theme, Value, Click).GetHashCode(); hash = (hash * 16777619) ^ Text.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IElement? element) => + Equals(element as ButtonElement); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ImageElement.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ImageElement.cs index b7a4f221..274add2f 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ImageElement.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ImageElement.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// An image element that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ImageElement : IElement, IEquatable +public class ImageElement : IElement, IEquatable, IEquatable { - internal ImageElement(string source, string alternative = null, ImageSize? size = null, bool? circle = null) + internal ImageElement(string source, string? alternative = null, ImageSize? size = null, bool? circle = null) { Source = source; Alternative = alternative; @@ -38,7 +39,7 @@ internal ImageElement(string source, string alternative = null, ImageSize? size /// /// A string that represents the alternative text of the image. /// - public string Alternative { get; } + public string? Alternative { get; } /// /// Gets the size of the image. @@ -64,30 +65,33 @@ internal ImageElement(string source, string alternative = null, ImageSize? size /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ImageElement left, ImageElement right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ImageElement? left, ImageElement? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ImageElement left, ImageElement right) - => !(left == right); + public static bool operator !=(ImageElement? left, ImageElement? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ImageElement imageElement && Equals(imageElement); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ImageElement imageElement && Equals(imageElement); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ImageElement imageElement) - => GetHashCode() == imageElement?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ImageElement? imageElement) => + GetHashCode() == imageElement?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Source, Alternative, Size, Circle).GetHashCode(); + public override int GetHashCode() => + (Type, Source, Alternative, Size, Circle).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElement? element) => + Equals(element as ImageElement); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/KMarkdownElement.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/KMarkdownElement.cs index e5f7ca30..0265d5f2 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/KMarkdownElement.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/KMarkdownElement.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,12 @@ namespace Kook; /// A KMarkdown element that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class KMarkdownElement : IElement, IEquatable +public class KMarkdownElement : IElement, IEquatable, IEquatable { - internal KMarkdownElement(string content) => Content = content; + internal KMarkdownElement(string content) + { + Content = content; + } /// /// Gets the type of the element. @@ -35,30 +39,33 @@ public class KMarkdownElement : IElement, IEquatable /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(KMarkdownElement left, KMarkdownElement right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(KMarkdownElement? left, KMarkdownElement? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(KMarkdownElement left, KMarkdownElement right) - => !(left == right); + public static bool operator !=(KMarkdownElement? left, KMarkdownElement? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is KMarkdownElement kMarkdownElement && Equals(kMarkdownElement); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is KMarkdownElement kMarkdownElement && Equals(kMarkdownElement); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(KMarkdownElement kMarkdownElement) - => GetHashCode() == kMarkdownElement?.GetHashCode(); + public bool Equals([NotNullWhen(true)] KMarkdownElement? kMarkdownElement) => + GetHashCode() == kMarkdownElement?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Content).GetHashCode(); + public override int GetHashCode() => + (Type, Content).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElement? element) => + Equals(element as KMarkdownElement); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ParagraphStruct.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ParagraphStruct.cs index f80272fc..b0b5094a 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ParagraphStruct.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/ParagraphStruct.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,9 +8,9 @@ namespace Kook; /// A paragraph struct that can be used in modules. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ParagraphStruct : IElement, IEquatable +public class ParagraphStruct : IElement, IEquatable, IEquatable { - internal ParagraphStruct(int columnCount, ImmutableArray fields) + internal ParagraphStruct(int? columnCount, ImmutableArray fields) { ColumnCount = columnCount; Fields = fields; @@ -29,7 +30,7 @@ internal ParagraphStruct(int columnCount, ImmutableArray fields) /// /// An int value that represents the number of columns in the paragraph. /// - public int ColumnCount { get; } + public int? ColumnCount { get; } /// /// Gets the fields in the paragraph. @@ -45,39 +46,42 @@ internal ParagraphStruct(int columnCount, ImmutableArray fields) /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ParagraphStruct left, ParagraphStruct right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ParagraphStruct? left, ParagraphStruct? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ParagraphStruct left, ParagraphStruct right) - => !(left == right); + public static bool operator !=(ParagraphStruct? left, ParagraphStruct? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ParagraphStruct paragraphStruct && Equals(paragraphStruct); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ParagraphStruct paragraphStruct && Equals(paragraphStruct); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ParagraphStruct paragraphStruct) - => GetHashCode() == paragraphStruct?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ParagraphStruct? paragraphStruct) => + GetHashCode() == paragraphStruct?.GetHashCode(); /// public override int GetHashCode() { unchecked { - int hash = (int)2166136261; + int hash = (int) 2166136261; hash = (hash * 16777619) ^ (Type, ColumnCount).GetHashCode(); - foreach (IElement element in Fields) hash = (hash * 16777619) ^ element.GetHashCode(); - + foreach (IElement element in Fields) + hash = (hash * 16777619) ^ element.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IElement? element) => + Equals(element as ParagraphStruct); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/PlainTextElement.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/PlainTextElement.cs index 3560862f..ba0f1efe 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Elements/PlainTextElement.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Elements/PlainTextElement.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// A plain text element that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class PlainTextElement : IElement, IEquatable +public class PlainTextElement : IElement, IEquatable, IEquatable { - internal PlainTextElement(string content, bool emoji) + internal PlainTextElement(string content, bool? emoji) { Content = content; Emoji = emoji; @@ -38,7 +39,7 @@ internal PlainTextElement(string content, bool emoji) /// true if the shortcuts should be translated into emojis; /// false if the text should be displayed as is. /// - public bool Emoji { get; } + public bool? Emoji { get; } /// public override string ToString() => Content; @@ -49,30 +50,33 @@ internal PlainTextElement(string content, bool emoji) /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(PlainTextElement left, PlainTextElement right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(PlainTextElement? left, PlainTextElement? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(PlainTextElement left, PlainTextElement right) - => !(left == right); + public static bool operator !=(PlainTextElement? left, PlainTextElement? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is PlainTextElement plainTextElement && Equals(plainTextElement); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is PlainTextElement plainTextElement && Equals(plainTextElement); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(PlainTextElement plainTextElement) - => GetHashCode() == plainTextElement?.GetHashCode(); + public bool Equals([NotNullWhen(true)] PlainTextElement? plainTextElement) => + GetHashCode() == plainTextElement?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Content, Emoji).GetHashCode(); + public override int GetHashCode() => + (Type, Content, Emoji).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IElement? element) => + Equals(element as PlainTextElement); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ActionGroupModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ActionGroupModule.cs index 061add35..4298c30f 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ActionGroupModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ActionGroupModule.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,9 +8,12 @@ namespace Kook; /// Represents an action group module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ActionGroupModule : IModule, IEquatable +public class ActionGroupModule : IModule, IEquatable, IEquatable { - internal ActionGroupModule(ImmutableArray elements) => Elements = elements; + internal ActionGroupModule(ImmutableArray elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.ActionGroup; @@ -28,28 +32,28 @@ public class ActionGroupModule : IModule, IEquatable /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ActionGroupModule left, ActionGroupModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ActionGroupModule left, ActionGroupModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ActionGroupModule left, ActionGroupModule right) - => !(left == right); + public static bool operator !=(ActionGroupModule left, ActionGroupModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ActionGroupModule actionGroupModule && Equals(actionGroupModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ActionGroupModule actionGroupModule && Equals(actionGroupModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ActionGroupModule actionGroupModule) - => GetHashCode() == actionGroupModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ActionGroupModule? actionGroupModule) => + GetHashCode() == actionGroupModule?.GetHashCode(); /// public override int GetHashCode() @@ -58,9 +62,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ Type.GetHashCode(); - foreach (ButtonElement buttonElement in Elements) hash = (hash * 16777619) ^ buttonElement.GetHashCode(); - + foreach (ButtonElement buttonElement in Elements) + hash = (hash * 16777619) ^ buttonElement.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as ActionGroupModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/AudioModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/AudioModule.cs index 9aa42fa8..3e771c45 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/AudioModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/AudioModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// Represents an audio module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class AudioModule : IMediaModule, IEquatable +public class AudioModule : IMediaModule, IEquatable, IEquatable { - internal AudioModule(string source, string title, string cover) + internal AudioModule(string source, string? title, string? cover) { Source = source; Title = title; @@ -22,7 +23,7 @@ internal AudioModule(string source, string title, string cover) public string Source { get; } /// - public string Title { get; } + public string? Title { get; } /// /// Gets the cover of the audio associated with this module. @@ -30,7 +31,7 @@ internal AudioModule(string source, string title, string cover) /// /// A string representing the cover of the audio associated with this module. /// - public string Cover { get; } + public string? Cover { get; } private string DebuggerDisplay => $"{Type}: {Title}"; @@ -38,30 +39,33 @@ internal AudioModule(string source, string title, string cover) /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(AudioModule left, AudioModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(AudioModule left, AudioModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(AudioModule left, AudioModule right) - => !(left == right); + public static bool operator !=(AudioModule left, AudioModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is AudioModule audioModule && Equals(audioModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is AudioModule audioModule && Equals(audioModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(AudioModule audioModule) - => GetHashCode() == audioModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] AudioModule? audioModule) => + GetHashCode() == audioModule?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Source, Title, Cover).GetHashCode(); + public override int GetHashCode() => + (Type, Source, Title, Cover).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as AudioModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ActionGroupModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ActionGroupModuleBuilder.cs index 9ab90c47..6bd6f677 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ActionGroupModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ActionGroupModuleBuilder.cs @@ -1,14 +1,12 @@ -using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Represents a action group module builder for creating an . /// -public class ActionGroupModuleBuilder : IModuleBuilder, IEquatable +public class ActionGroupModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private List _elements; - /// /// Returns the maximum number of elements allowed by Kook. /// @@ -17,12 +15,18 @@ public class ActionGroupModuleBuilder : IModuleBuilder, IEquatable /// Initializes a new instance of the class. /// - public ActionGroupModuleBuilder() => Elements = new List(); + public ActionGroupModuleBuilder() + { + Elements = []; + } /// /// Initializes a new instance of the class. /// - public ActionGroupModuleBuilder(List elements) => Elements = elements; + public ActionGroupModuleBuilder(IList elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.ActionGroup; @@ -30,29 +34,10 @@ public class ActionGroupModuleBuilder : IModuleBuilder, IEquatable /// Gets or sets the button elements of the action group module. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// /// - /// A containing the button elements of the action group module. + /// An containing the button elements of the action group module. /// - public List Elements - { - get => _elements; - set - { - if (value is null) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null."); - if (value.Count > MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(Elements)); - - _elements = value; - } - } + public IList Elements { get; set; } /// /// Adds a button element to the action group module. @@ -63,16 +48,8 @@ public List Elements /// /// The current builder. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// public ActionGroupModuleBuilder AddElement(ButtonElementBuilder field) { - if (Elements.Count >= MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(field)); - Elements.Add(field); return this; } @@ -89,11 +66,11 @@ public ActionGroupModuleBuilder AddElement(ButtonElementBuilder field) /// /// The addition operation would cause the number of elements to exceed . /// - public ActionGroupModuleBuilder AddElement(Action action) + public ActionGroupModuleBuilder AddElement(Action? action = null) { ButtonElementBuilder field = new(); - action(field); - AddElement(field); + action?.Invoke(field); + Elements.Add(field); return this; } @@ -103,13 +80,27 @@ public ActionGroupModuleBuilder AddElement(Action action) /// /// An representing the built action group module object. /// + /// + /// is null. + /// + /// + /// is an empty list. + /// + /// + /// The number of elements of is greater than . + /// public ActionGroupModule Build() { - if (Elements is null or { Count: 0 }) + if (Elements == null) throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null or empty list."); - return new ActionGroupModule(Elements.Select(e => e.Build()).ToImmutableArray()); + nameof(Elements), "Element cannot be null or empty list."); + if (Elements.Count == 0) + throw new ArgumentException( + "Element cannot be null or empty list.", nameof(Elements)); + if (Elements.Count > MaxElementCount) + throw new ArgumentException( + $"Element count must be less than or equal to {MaxElementCount}.", nameof(Elements)); + return new ActionGroupModule([..Elements.Select(e => e.Build())]); } /// @@ -119,39 +110,45 @@ public ActionGroupModule Build() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ActionGroupModuleBuilder left, ActionGroupModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ActionGroupModuleBuilder? left, ActionGroupModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ActionGroupModuleBuilder left, ActionGroupModuleBuilder right) - => !(left == right); + public static bool operator !=(ActionGroupModuleBuilder? left, ActionGroupModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ActionGroupModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ActionGroupModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ActionGroupModuleBuilder actionGroupModuleBuilder) + public bool Equals([NotNullWhen(true)] ActionGroupModuleBuilder? actionGroupModuleBuilder) { - if (actionGroupModuleBuilder is null) return false; + if (actionGroupModuleBuilder is null) + return false; - if (Elements.Count != actionGroupModuleBuilder.Elements.Count) return false; + if (Elements.Count != actionGroupModuleBuilder.Elements.Count) + return false; - for (int i = 0; i < Elements.Count; i++) - if (Elements[i] != actionGroupModuleBuilder.Elements[i]) - return false; + if (Elements + .Zip(actionGroupModuleBuilder.Elements, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == actionGroupModuleBuilder.Type; } /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as ActionGroupModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/AudioModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/AudioModuleBuilder.cs index 50823da2..8fd7a79a 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/AudioModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/AudioModuleBuilder.cs @@ -1,11 +1,12 @@ +using System.Diagnostics.CodeAnalysis; using Kook.Utils; namespace Kook; /// -/// Represents a audio module builder for creating an . +/// Represents an audio module builder for creating an . /// -public class AudioModuleBuilder : IModuleBuilder, IEquatable +public class AudioModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.Audio; @@ -23,11 +24,11 @@ public AudioModuleBuilder() /// The source URL of the video. /// The cover URL of the video. /// The title of the video. - public AudioModuleBuilder(string source, string cover = null, string title = null) + public AudioModuleBuilder(string source, string? cover = null, string? title = null) { - WithSource(source); - WithCover(cover); - WithTitle(title); + Source = source; + Cover = cover; + Title = title; } /// @@ -36,7 +37,7 @@ public AudioModuleBuilder(string source, string cover = null, string title = nul /// /// The source URL of the video. /// - public string Source { get; set; } + public string? Source { get; set; } /// /// Gets or sets the cover URL of the video. @@ -44,7 +45,7 @@ public AudioModuleBuilder(string source, string cover = null, string title = nul /// /// The cover URL of the video. /// - public string Cover { get; set; } + public string? Cover { get; set; } /// /// Gets or sets the title of the video. @@ -52,7 +53,7 @@ public AudioModuleBuilder(string source, string cover = null, string title = nul /// /// The title of the video. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// Sets the source URL of the video. @@ -63,7 +64,7 @@ public AudioModuleBuilder(string source, string cover = null, string title = nul /// /// The current builder. /// - public AudioModuleBuilder WithSource(string source) + public AudioModuleBuilder WithSource(string? source) { Source = source; return this; @@ -78,7 +79,7 @@ public AudioModuleBuilder WithSource(string source) /// /// The current builder. /// - public AudioModuleBuilder WithCover(string cover) + public AudioModuleBuilder WithCover(string? cover) { Cover = cover; return this; @@ -93,7 +94,7 @@ public AudioModuleBuilder WithCover(string cover) /// /// The current builder. /// - public AudioModuleBuilder WithTitle(string title) + public AudioModuleBuilder WithTitle(string? title) { Title = title; return this; @@ -105,56 +106,63 @@ public AudioModuleBuilder WithTitle(string title) /// /// An representing the built audio module object. /// - /// - /// does not include a protocol (neither HTTP nor HTTPS) - /// - /// - /// does not include a protocol (neither HTTP nor HTTPS) + /// + /// cannot be null /// /// - /// cannot be null or empty + /// cannot be empty /// - /// - /// cannot be null or empty + /// + /// is not a valid URL + /// + /// + /// is not a valid URL /// + [MemberNotNull(nameof(Source))] public AudioModule Build() { - if (!UrlValidation.Validate(Source)) throw new ArgumentException("The link to a file cannot be null or empty.", nameof(Source)); - - UrlValidation.Validate(Cover); + if (Source == null) + throw new ArgumentNullException(nameof(Source), "The source url cannot be null."); + if (string.IsNullOrEmpty(Source)) + throw new ArgumentException("The source url cannot be empty.", nameof(Source)); + UrlValidation.Validate(Source); + if (Cover != null) + UrlValidation.Validate(Cover); return new AudioModule(Source, Title, Cover); } /// + [MemberNotNull(nameof(Source))] IModule IModuleBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(AudioModuleBuilder left, AudioModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(AudioModuleBuilder? left, AudioModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(AudioModuleBuilder left, AudioModuleBuilder right) - => !(left == right); + public static bool operator !=(AudioModuleBuilder? left, AudioModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is AudioModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is AudioModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(AudioModuleBuilder audioModuleBuilder) + public bool Equals([NotNullWhen(true)] AudioModuleBuilder? audioModuleBuilder) { - if (audioModuleBuilder is null) return false; + if (audioModuleBuilder is null) + return false; return Type == audioModuleBuilder.Type && Source == audioModuleBuilder.Source @@ -164,4 +172,7 @@ public bool Equals(AudioModuleBuilder audioModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as AudioModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContainerModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContainerModuleBuilder.cs index 9dde0104..0741d648 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContainerModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContainerModuleBuilder.cs @@ -1,14 +1,12 @@ -using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Represents a container module builder for creating a . /// -public class ContainerModuleBuilder : IModuleBuilder, IEquatable +public class ContainerModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private List _elements; - /// /// Returns the maximum number of elements allowed by Kook. /// @@ -17,12 +15,18 @@ public class ContainerModuleBuilder : IModuleBuilder, IEquatable /// Initializes a new instance of the class. /// - public ContainerModuleBuilder() => Elements = new List(); + public ContainerModuleBuilder() + { + Elements = []; + } /// /// Initializes a new instance of the class. /// - public ContainerModuleBuilder(List elements) => Elements = elements; + public ContainerModuleBuilder(IList elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.Container; @@ -30,29 +34,10 @@ public class ContainerModuleBuilder : IModuleBuilder, IEquatable /// Gets or sets the image elements in the container module. /// - /// - /// The number of is greater than . - /// /// - /// A containing the image elements in this image container module. + /// An containing the image elements in this image container module. /// - public List Elements - { - get => _elements; - set - { - if (value is null) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null."); - if (value.Count > MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(Elements)); - - _elements = value; - } - } + public IList Elements { get; set; } /// /// Adds an image element to the container module. @@ -63,16 +48,8 @@ public List Elements /// /// The current builder. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// public ContainerModuleBuilder AddElement(ImageElementBuilder field) { - if (Elements.Count >= MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(field)); - Elements.Add(field); return this; } @@ -86,14 +63,11 @@ public ContainerModuleBuilder AddElement(ImageElementBuilder field) /// /// The current builder. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// - public ContainerModuleBuilder AddElement(Action action) + public ContainerModuleBuilder AddElement(Action? action = null) { ImageElementBuilder field = new(); - action(field); - AddElement(field); + action?.Invoke(field); + Elements.Add(field); return this; } @@ -103,13 +77,29 @@ public ContainerModuleBuilder AddElement(Action action) /// /// A representing the built container module object. /// + /// + /// cannot be null. + /// + /// + /// cannot be an empty list. + /// + /// + /// count must be less than or equal to . + /// public ContainerModule Build() { - if (Elements is null or { Count: 0 }) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null or empty list."); - return new ContainerModule(Elements.Select(e => e.Build()).ToImmutableArray()); + if (Elements is null) + throw new ArgumentNullException(nameof(Elements), "Element cannot be null."); + + if (Elements.Count == 0) + throw new ArgumentException("Element cannot be an empty list.", nameof(Elements)); + + if (Elements.Count > MaxElementCount) + throw new ArgumentException( + $"Element count must be less than or equal to {MaxElementCount}.", + nameof(Elements)); + + return new ContainerModule([..Elements.Select(e => e.Build())]); } /// @@ -119,39 +109,45 @@ public ContainerModule Build() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ContainerModuleBuilder left, ContainerModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ContainerModuleBuilder? left, ContainerModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ContainerModuleBuilder left, ContainerModuleBuilder right) - => !(left == right); + public static bool operator !=(ContainerModuleBuilder? left, ContainerModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ContainerModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ContainerModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ContainerModuleBuilder containerModuleBuilder) + public bool Equals([NotNullWhen(true)] ContainerModuleBuilder? containerModuleBuilder) { - if (containerModuleBuilder is null) return false; + if (containerModuleBuilder is null) + return false; - if (Elements.Count != containerModuleBuilder.Elements.Count) return false; + if (Elements.Count != containerModuleBuilder.Elements.Count) + return false; - for (int i = 0; i < Elements.Count; i++) - if (Elements[i] != containerModuleBuilder.Elements[i]) - return false; + if (Elements + .Zip(containerModuleBuilder.Elements, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == containerModuleBuilder.Type; } /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as ContainerModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContextModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContextModuleBuilder.cs index b27e8f69..a0181317 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContextModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ContextModuleBuilder.cs @@ -1,14 +1,13 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Represents a context module builder for creating a . /// -public class ContextModuleBuilder : IModuleBuilder, IEquatable +public class ContextModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private List _elements; - /// /// Returns the maximum number of elements allowed by Kook. /// @@ -17,12 +16,18 @@ public class ContextModuleBuilder : IModuleBuilder, IEquatable /// Initializes a new instance of the class. /// - public ContextModuleBuilder() => Elements = new List(); + public ContextModuleBuilder() + { + Elements = []; + } /// /// Initializes a new instance of the class. /// - public ContextModuleBuilder(List elements) => Elements = elements; + public ContextModuleBuilder(IList elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.Context; @@ -30,38 +35,7 @@ public class ContextModuleBuilder : IModuleBuilder, IEquatable /// Gets or sets the elements of the context module. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// - /// - /// The contains disallowed type of element builder. Allowed element builders are - /// , , and . - /// - public List Elements - { - get => _elements; - set - { - if (value is null) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null."); - - if (value.Count > MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(Elements)); - - if (value.Exists(e => e.Type != ElementType.PlainText - && e.Type != ElementType.KMarkdown - && e.Type != ElementType.Image)) - throw new ArgumentException( - "Elements must be of type PlainText, KMarkdown or Image.", - nameof(Elements)); - - _elements = value; - } - } + public IList Elements { get; set; } /// /// Adds a PlainText element to the context module. @@ -77,11 +51,6 @@ public List Elements /// public ContextModuleBuilder AddElement(PlainTextElementBuilder field) { - if (Elements.Count >= MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(field)); - Elements.Add(field); return this; } @@ -95,16 +64,8 @@ public ContextModuleBuilder AddElement(PlainTextElementBuilder field) /// /// The current builder. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// public ContextModuleBuilder AddElement(KMarkdownElementBuilder field) { - if (Elements.Count >= MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(field)); - Elements.Add(field); return this; } @@ -123,11 +84,6 @@ public ContextModuleBuilder AddElement(KMarkdownElementBuilder field) /// public ContextModuleBuilder AddElement(ImageElementBuilder field) { - if (Elements.Count >= MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(field)); - Elements.Add(field); return this; } @@ -141,31 +97,12 @@ public ContextModuleBuilder AddElement(ImageElementBuilder field) /// /// The current builder. /// - /// - /// The addition operation would cause the number of elements to exceed . - /// - public ContextModuleBuilder AddElement(Action action = null) + public ContextModuleBuilder AddElement(Action? action = null) where T : IElementBuilder, new() { T field = new(); action?.Invoke(field); - switch (field) - { - case PlainTextElementBuilder plainText: - AddElement(plainText); - break; - case KMarkdownElementBuilder kMarkdown: - AddElement(kMarkdown); - break; - case ImageElementBuilder image: - AddElement(image); - break; - default: - throw new ArgumentException( - "Elements of contexts must be of type PlainText, KMarkdown or Image.", - nameof(action)); - } - + Elements.Add(field); return this; } @@ -175,8 +112,35 @@ public ContextModuleBuilder AddElement(Action action = null) /// /// A representing the built context module object. /// - public ContextModule Build() => - new(Elements.Select(e => e.Build()).ToImmutableArray()); + /// + /// The is null. + /// + /// + /// The count is greater than . + /// + /// + /// The contain an element that is not a , + /// , or . + /// + public ContextModule Build() + { + if (Elements is null) + throw new ArgumentNullException( + nameof(Elements), + "Element cannot be null."); + + if (Elements.Count > MaxElementCount) + throw new ArgumentException( + $"Element count must be less than or equal to {MaxElementCount}.", + nameof(Elements)); + + if (Elements.Any(e => e is not (PlainTextElementBuilder or KMarkdownElementBuilder or ImageElementBuilder))) + throw new ArgumentException( + "Elements must be of type PlainText, KMarkdown or Image.", + nameof(Elements)); + + return new ContextModule([..Elements.Select(e => e.Build())]); + } /// IModule IModuleBuilder.Build() => Build(); @@ -185,39 +149,45 @@ public ContextModule Build() => /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ContextModuleBuilder left, ContextModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ContextModuleBuilder? left, ContextModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ContextModuleBuilder left, ContextModuleBuilder right) - => !(left == right); + public static bool operator !=(ContextModuleBuilder? left, ContextModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ContextModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ContextModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ContextModuleBuilder contextModuleBuilder) + public bool Equals([NotNullWhen(true)] ContextModuleBuilder? contextModuleBuilder) { - if (contextModuleBuilder is null) return false; + if (contextModuleBuilder is null) + return false; - if (Elements.Count != contextModuleBuilder.Elements.Count) return false; + if (Elements.Count != contextModuleBuilder.Elements.Count) + return false; - for (int i = 0; i < Elements.Count; i++) - if (Elements[i] != contextModuleBuilder.Elements[i]) - return false; + if (Elements + .Zip(contextModuleBuilder.Elements, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == contextModuleBuilder.Type; } /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as ContextModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/CountdownModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/CountdownModuleBuilder.cs index 9155717b..6af0380c 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/CountdownModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/CountdownModuleBuilder.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// Represents a countdown module builder for creating a . /// -public class CountdownModuleBuilder : IModuleBuilder, IEquatable +public class CountdownModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.Countdown; @@ -20,10 +22,9 @@ public CountdownModuleBuilder() /// public CountdownModuleBuilder(CountdownMode mode, DateTimeOffset endTime, DateTimeOffset? startTime = null) { - WithMode(mode); - WithEndTime(endTime); - if (startTime.HasValue) - WithStartTime(startTime.Value); + Mode = mode; + EndTime = endTime; + StartTime = startTime; } /// @@ -89,7 +90,7 @@ public CountdownModuleBuilder WithEndTime(DateTimeOffset endTime) /// /// The current builder. /// - public CountdownModuleBuilder WithStartTime(DateTimeOffset startTime) + public CountdownModuleBuilder WithStartTime(DateTimeOffset? startTime) { StartTime = startTime; return this; @@ -102,6 +103,9 @@ public CountdownModuleBuilder WithStartTime(DateTimeOffset startTime) /// A representing the built countdown module object. /// /// + /// is not but is set. + /// + /// /// is before the current time. /// /// @@ -118,8 +122,8 @@ public CountdownModule Build() if (EndTime < DateTimeOffset.Now) throw new ArgumentOutOfRangeException( - message: $"{nameof(EndTime)} must be equal or later than current timestamp.", - paramName: nameof(EndTime)); + nameof(EndTime), + $"{nameof(EndTime)} must be equal or later than current timestamp."); if (StartTime is not null && StartTime < DateTimeOffset.FromUnixTimeSeconds(0)) throw new ArgumentOutOfRangeException( @@ -141,27 +145,27 @@ public CountdownModule Build() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(CountdownModuleBuilder left, CountdownModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(CountdownModuleBuilder? left, CountdownModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(CountdownModuleBuilder left, CountdownModuleBuilder right) - => !(left == right); + public static bool operator !=(CountdownModuleBuilder? left, CountdownModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is CountdownModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is CountdownModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(CountdownModuleBuilder countdownModuleBuilder) + public bool Equals([NotNullWhen(true)] CountdownModuleBuilder? countdownModuleBuilder) { if (countdownModuleBuilder is null) return false; @@ -173,4 +177,7 @@ public bool Equals(CountdownModuleBuilder countdownModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as CountdownModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/DividerModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/DividerModuleBuilder.cs index 397ca53a..a3bafead 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/DividerModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/DividerModuleBuilder.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// Represents a divider module builder for creating a . /// -public class DividerModuleBuilder : IModuleBuilder, IEquatable +public class DividerModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.Divider; @@ -23,33 +25,37 @@ public class DividerModuleBuilder : IModuleBuilder, IEquatable is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(DividerModuleBuilder left, DividerModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(DividerModuleBuilder? left, DividerModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(DividerModuleBuilder left, DividerModuleBuilder right) - => !(left == right); + public static bool operator !=(DividerModuleBuilder? left, DividerModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is DividerModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is DividerModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(DividerModuleBuilder dividerModuleBuilder) + public bool Equals([NotNullWhen(true)] DividerModuleBuilder? dividerModuleBuilder) { - if (dividerModuleBuilder is null) return false; + if (dividerModuleBuilder is null) + return false; return Type == dividerModuleBuilder.Type; } /// public override int GetHashCode() => base.GetHashCode(); -} \ No newline at end of file + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as DividerModuleBuilder); +} diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/FileModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/FileModuleBuilder.cs index 1b525b89..71b1578c 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/FileModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/FileModuleBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Kook.Utils; namespace Kook; @@ -5,7 +6,7 @@ namespace Kook; /// /// Represents a file module builder for creating a . /// -public class FileModuleBuilder : IModuleBuilder, IEquatable +public class FileModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.File; @@ -22,10 +23,10 @@ public FileModuleBuilder() /// /// The source URL of the file. /// The title of the file. - public FileModuleBuilder(string source, string title = null) + public FileModuleBuilder(string source, string? title = null) { - WithSource(source); - WithTitle(title); + Source = source; + Title = title; } /// @@ -34,7 +35,7 @@ public FileModuleBuilder(string source, string title = null) /// /// The source URL of the file. /// - public string Source { get; set; } + public string? Source { get; set; } /// /// Gets or sets the title of the file. @@ -42,7 +43,7 @@ public FileModuleBuilder(string source, string title = null) /// /// The title of the file. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// Sets the source URL of the file. @@ -53,7 +54,7 @@ public FileModuleBuilder(string source, string title = null) /// /// The current builder. /// - public FileModuleBuilder WithSource(string source) + public FileModuleBuilder WithSource(string? source) { Source = source; return this; @@ -80,47 +81,57 @@ public FileModuleBuilder WithTitle(string title) /// /// A representing the built file module object. /// - /// - /// does not include a protocol (neither HTTP nor HTTPS) + /// + /// The url is null. /// /// - /// cannot be null or empty + /// The url is empty. /// + /// + /// The url does not include a protocol (either HTTP or HTTPS). + /// + [MemberNotNull(nameof(Source))] public FileModule Build() { - if (!UrlValidation.Validate(Source)) throw new ArgumentException("The link to a file cannot be null or empty.", nameof(Source)); + if (Source == null) + throw new ArgumentNullException(nameof(Source), "The source url cannot be null or empty."); + if (string.IsNullOrEmpty(Source)) + throw new ArgumentException("The source url cannot be null or empty.", nameof(Source)); + + UrlValidation.Validate(Source); return new FileModule(Source, Title); } /// + [MemberNotNull(nameof(Source))] IModule IModuleBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(FileModuleBuilder left, FileModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(FileModuleBuilder? left, FileModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(FileModuleBuilder left, FileModuleBuilder right) - => !(left == right); + public static bool operator !=(FileModuleBuilder? left, FileModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is FileModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is FileModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(FileModuleBuilder fileModuleBuilder) + public bool Equals([NotNullWhen(true)] FileModuleBuilder? fileModuleBuilder) { if (fileModuleBuilder is null) return false; @@ -131,4 +142,7 @@ public bool Equals(FileModuleBuilder fileModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as FileModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/HeaderModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/HeaderModuleBuilder.cs index fbdd635a..4a556f28 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/HeaderModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/HeaderModuleBuilder.cs @@ -1,12 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// Represents a header module builder for creating a . /// -public class HeaderModuleBuilder : IModuleBuilder, IEquatable +public class HeaderModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private PlainTextElementBuilder _text; - /// /// Gets the maximum content length for header allowed by Kook. /// @@ -26,41 +26,27 @@ public HeaderModuleBuilder() /// Initializes a new instance of the class. /// /// The text to be set for the header. - public HeaderModuleBuilder(PlainTextElementBuilder text) => WithText(text); + public HeaderModuleBuilder(PlainTextElementBuilder text) + { + Text = text; + } /// /// Initializes a new instance of the class. /// /// The text to be set for the header. - public HeaderModuleBuilder(string text) => WithText(text); + public HeaderModuleBuilder(string text) + { + Text = new PlainTextElementBuilder(text); + } /// /// Gets or sets the text of the header. /// - /// - /// The length of is greater than .", - /// /// /// A representing the text of the header. /// - public PlainTextElementBuilder Text - { - get => _text; - set - { - if (value is not null && value.Content is null) - throw new ArgumentException( - "Header content cannot be null.", - nameof(Text)); - - if (value is not null && value.Content.Length > MaxTitleContentLength) - throw new ArgumentException( - $"Header content length must be less than or equal to {MaxTitleContentLength}.", - nameof(Text)); - - _text = value; - } - } + public PlainTextElementBuilder? Text { get; set; } /// /// Sets the text of the header. @@ -71,9 +57,6 @@ public PlainTextElementBuilder Text /// /// The current builder. /// - /// - /// The length of is greater than .", - /// public HeaderModuleBuilder WithText(PlainTextElementBuilder text) { Text = text; @@ -85,12 +68,9 @@ public HeaderModuleBuilder WithText(PlainTextElementBuilder text) /// /// The text to be set for the header. /// The current builder. - /// - /// The length of is greater than .", - /// public HeaderModuleBuilder WithText(string text) { - Text = new PlainTextElementBuilder().WithContent(text); + Text = new PlainTextElementBuilder(text); return this; } @@ -100,16 +80,13 @@ public HeaderModuleBuilder WithText(string text) /// /// The action to set the text of the header. /// - /// - /// The length of result of is greater than .", - /// /// /// The current builder. /// - public HeaderModuleBuilder WithText(Action action) + public HeaderModuleBuilder WithText(Action? action = null) { PlainTextElementBuilder text = new(); - action(text); + action?.Invoke(text); Text = text; return this; } @@ -120,7 +97,31 @@ public HeaderModuleBuilder WithText(Action action) /// /// A representing the built header module object. /// - public HeaderModule Build() => new(Text?.Build()); + /// + /// The is null. + /// + /// + /// The is null. + /// + /// + /// The content is longer than . + /// + [MemberNotNull(nameof(Text))] + public HeaderModule Build() + { + if (Text is null) + throw new ArgumentNullException(nameof(Text), "The header text cannot be null."); + + if (Text.Content is null) + throw new ArgumentException("The content of the header text cannot be null.", nameof(Text)); + + if (Text.Content.Length > MaxTitleContentLength) + throw new ArgumentException( + $"Header content length must be less than or equal to {MaxTitleContentLength}.", + nameof(Text)); + + return new HeaderModule(Text.Build()); + } /// /// Initialized a new instance of the class @@ -132,39 +133,40 @@ public HeaderModuleBuilder WithText(Action action) /// /// An object that is initialized with the specified . /// - public static implicit operator HeaderModuleBuilder(string text) - => new HeaderModuleBuilder().WithText(b => b.WithContent(text)); + public static implicit operator HeaderModuleBuilder(string text) => new(text); /// + [MemberNotNull(nameof(Text))] IModule IModuleBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(HeaderModuleBuilder left, HeaderModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(HeaderModuleBuilder? left, HeaderModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(HeaderModuleBuilder left, HeaderModuleBuilder right) - => !(left == right); + public static bool operator !=(HeaderModuleBuilder? left, HeaderModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is HeaderModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is HeaderModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(HeaderModuleBuilder headerModuleBuilder) + public bool Equals([NotNullWhen(true)] HeaderModuleBuilder? headerModuleBuilder) { - if (headerModuleBuilder is null) return false; + if (headerModuleBuilder == null) + return false; return Type == headerModuleBuilder.Type && Text == headerModuleBuilder.Text; @@ -172,4 +174,7 @@ public bool Equals(HeaderModuleBuilder headerModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as HeaderModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ImageGroupModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ImageGroupModuleBuilder.cs index 3d6e486a..78ed6087 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ImageGroupModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/ImageGroupModuleBuilder.cs @@ -1,14 +1,13 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Representing an image group module builder for create an . /// -public class ImageGroupModuleBuilder : IModuleBuilder, IEquatable +public class ImageGroupModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private List _elements; - /// /// Returns the maximum number of elements allowed by Kook. /// @@ -17,12 +16,18 @@ public class ImageGroupModuleBuilder : IModuleBuilder, IEquatable /// Initializes a new instance of the class. /// - public ImageGroupModuleBuilder() => Elements = new List(); + public ImageGroupModuleBuilder() + { + Elements = []; + } /// /// Initializes a new instance of the class. /// - public ImageGroupModuleBuilder(List elements) => Elements = elements; + public ImageGroupModuleBuilder(IList elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.ImageGroup; @@ -36,23 +41,7 @@ public class ImageGroupModuleBuilder : IModuleBuilder, IEquatable /// An containing the elements of the image group. /// - public List Elements - { - get => _elements; - set - { - if (value is null) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null."); - if (value.Count > MaxElementCount) - throw new ArgumentException( - $"Element count must be less than or equal to {MaxElementCount}.", - nameof(Elements)); - - _elements = value; - } - } + public IList Elements { get; set; } /// /// Adds an image element to the image group. @@ -103,13 +92,29 @@ public ImageGroupModuleBuilder AddElement(Action action) /// /// An representing the built image group module object. /// + /// + /// The is null. + /// + /// + /// The is an empty list. + /// + /// + /// Element count is greater than . + /// public ImageGroupModule Build() { - if (Elements is null or { Count: 0 }) - throw new ArgumentNullException( - nameof(Elements), - "Element cannot be null or empty list."); - return new ImageGroupModule(Elements.Select(e => e.Build()).ToImmutableArray()); + if (Elements is null) + throw new ArgumentNullException(nameof(Elements), "Element cannot be null."); + + if (Elements.Count == 0) + throw new ArgumentException("Element cannot be an empty list.", nameof(Elements)); + + if (Elements.Count > MaxElementCount) + throw new ArgumentException( + $"Element count must be less than or equal to {MaxElementCount}.", + nameof(Elements)); + + return new ImageGroupModule([..Elements.Select(e => e.Build())]); } /// @@ -119,39 +124,45 @@ public ImageGroupModule Build() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ImageGroupModuleBuilder left, ImageGroupModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ImageGroupModuleBuilder? left, ImageGroupModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ImageGroupModuleBuilder left, ImageGroupModuleBuilder right) - => !(left == right); + public static bool operator !=(ImageGroupModuleBuilder? left, ImageGroupModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ImageGroupModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ImageGroupModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ImageGroupModuleBuilder imageGroupModuleBuilder) + public bool Equals([NotNullWhen(true)] ImageGroupModuleBuilder? imageGroupModuleBuilder) { - if (imageGroupModuleBuilder is null) return false; + if (imageGroupModuleBuilder is null) + return false; - if (Elements.Count != imageGroupModuleBuilder.Elements.Count) return false; + if (Elements.Count != imageGroupModuleBuilder.Elements.Count) + return false; - for (int i = 0; i < Elements.Count; i++) - if (Elements[i] != imageGroupModuleBuilder.Elements[i]) - return false; + if (Elements + .Zip(imageGroupModuleBuilder.Elements, (x, y) => (x, y)) + .Any(pair => pair.x != pair.y)) + return false; return Type == imageGroupModuleBuilder.Type; } /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as ImageGroupModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/InviteModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/InviteModuleBuilder.cs index cd1cecb2..88c9ad63 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/InviteModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/InviteModuleBuilder.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// Represents a invite module builder for creating an . /// -public class InviteModuleBuilder : IModuleBuilder, IEquatable +public class InviteModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.Invite; @@ -19,7 +21,10 @@ public InviteModuleBuilder() /// Initializes a new instance of the class. /// /// - public InviteModuleBuilder(string code) => WithCode(code); + public InviteModuleBuilder(string code) + { + Code = code; + } /// /// Gets or sets the code of the invite. @@ -27,7 +32,7 @@ public InviteModuleBuilder() /// /// A string representing the code of the invite. /// - public string Code { get; set; } + public string? Code { get; set; } /// /// Sets the code of the invite. @@ -50,7 +55,21 @@ public InviteModuleBuilder WithCode(string code) /// /// An representing the built invite module object. /// - public InviteModule Build() => new(Code); + /// + /// The is null. + /// + /// + /// The is empty or whitespace. + /// + [MemberNotNull(nameof(Code))] + public InviteModule Build() + { + if (Code == null) + throw new ArgumentNullException(nameof(Code), "The code of the invite cannot be null."); + if (string.IsNullOrWhiteSpace(Code)) + throw new ArgumentException("The code of the invite cannot be empty or whitespace.", nameof(Code)); + return new InviteModule(Code); + } /// /// Initialized a new instance of the class @@ -62,37 +81,37 @@ public InviteModuleBuilder WithCode(string code) /// /// An object that is initialized with the specified . /// - public static implicit operator InviteModuleBuilder(string code) => new InviteModuleBuilder() - .WithCode(code); + public static implicit operator InviteModuleBuilder(string code) => new(code); /// + [MemberNotNull(nameof(Code))] IModule IModuleBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(InviteModuleBuilder left, InviteModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(InviteModuleBuilder? left, InviteModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(InviteModuleBuilder left, InviteModuleBuilder right) - => !(left == right); + public static bool operator !=(InviteModuleBuilder? left, InviteModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is InviteModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is InviteModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(InviteModuleBuilder inviteModuleBuilder) + public bool Equals([NotNullWhen(true)] InviteModuleBuilder? inviteModuleBuilder) { if (inviteModuleBuilder is null) return false; @@ -102,4 +121,7 @@ public bool Equals(InviteModuleBuilder inviteModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as InviteModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/SectionModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/SectionModuleBuilder.cs index bff16201..447feebd 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/SectionModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/SectionModuleBuilder.cs @@ -1,13 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; /// /// Represents a section module builder for creating a . /// -public class SectionModuleBuilder : IModuleBuilder, IEquatable +public class SectionModuleBuilder : IModuleBuilder, IEquatable, IEquatable { - private IElementBuilder _text; - private IElementBuilder _accessory; - /// public ModuleType Type => ModuleType.Section; @@ -27,11 +26,11 @@ public SectionModuleBuilder() /// and ; or the is neither /// an nor . /// - public SectionModuleBuilder(IElementBuilder text, - SectionAccessoryMode mode = SectionAccessoryMode.Unspecified, IElementBuilder accessory = null) + public SectionModuleBuilder(IElementBuilder? text, + SectionAccessoryMode? mode = null, IElementBuilder? accessory = null) { Text = text; - WithMode(mode); + Mode = mode; Accessory = accessory; } @@ -44,11 +43,11 @@ public SectionModuleBuilder(IElementBuilder text, /// and ; or the is neither /// an nor . /// - public SectionModuleBuilder(string text, bool isKMarkdown = false, - SectionAccessoryMode mode = SectionAccessoryMode.Unspecified, IElementBuilder accessory = null) + public SectionModuleBuilder(string? text, bool isKMarkdown = false, + SectionAccessoryMode? mode = null, IElementBuilder? accessory = null) { WithText(text, isKMarkdown); - WithMode(mode); + Mode = mode; Accessory = accessory; } @@ -59,7 +58,7 @@ public SectionModuleBuilder(string text, bool isKMarkdown = false, /// A representing /// how the is positioned relative to the . /// - public SectionAccessoryMode Mode { get; set; } + public SectionAccessoryMode? Mode { get; set; } /// /// Gets or sets the text of the section. @@ -71,22 +70,7 @@ public SectionModuleBuilder(string text, bool isKMarkdown = false, /// The is not any form of text element, /// including , , and . /// - public IElementBuilder Text - { - get => _text; - set - { - if (value is not null - && value.Type != ElementType.PlainText - && value.Type != ElementType.KMarkdown - && value.Type != ElementType.Paragraph) - throw new ArgumentException( - "Section text must be a PlainText element, a KMarkdown element or a Paragraph struct.", - nameof(value)); - - _text = value; - } - } + public IElementBuilder? Text { get; set; } /// /// Gets or sets the accessory of the section. @@ -98,19 +82,7 @@ public IElementBuilder Text /// The is neither an /// nor . /// - public IElementBuilder Accessory - { - get => _accessory; - set - { - if (value is not null && value.Type != ElementType.Image && value.Type != ElementType.Button) - throw new ArgumentException( - $"Section text must be an {nameof(ImageElementBuilder)} or a {nameof(ButtonElement)}.", - nameof(value)); - - _accessory = value; - } - } + public IElementBuilder? Accessory { get; set; } /// /// Sets the text of the section. @@ -154,12 +126,12 @@ public SectionModuleBuilder WithText(KMarkdownElementBuilder text) /// /// The current builder. /// - public SectionModuleBuilder WithText(string text, bool isKMarkdown = false) + public SectionModuleBuilder WithText(string? text, bool isKMarkdown = false) { Text = isKMarkdown switch { - false => new PlainTextElementBuilder().WithContent(text), - true => new KMarkdownElementBuilder().WithContent(text) + false => new PlainTextElementBuilder(text), + true => new KMarkdownElementBuilder(text) }; return this; } @@ -188,7 +160,7 @@ public SectionModuleBuilder WithText(ParagraphStructBuilder text) /// /// The current builder. /// - public SectionModuleBuilder WithText(Action action = null) + public SectionModuleBuilder WithText(Action? action = null) where T : IElementBuilder, new() { T text = new(); @@ -236,7 +208,7 @@ public SectionModuleBuilder WithAccessory(ButtonElementBuilder accessory) /// /// The current builder. /// - public SectionModuleBuilder WithAccessory(Action action = null) + public SectionModuleBuilder WithAccessory(Action? action = null) where T : IElementBuilder, new() { T accessory = new(); @@ -266,12 +238,31 @@ public SectionModuleBuilder WithMode(SectionAccessoryMode mode) /// /// A representing the built section module object. /// + /// + /// The is not any form of text element, + /// including , , + /// and . + /// + /// + /// The is neither an + /// nor . + /// /// - /// The was not positioned to the right of the , + /// The was not positioned to the left of the , /// which is not allowed. /// public SectionModule Build() { + if (Text is not (null or PlainTextElementBuilder or KMarkdownElementBuilder or ParagraphStructBuilder)) + throw new ArgumentException( + "Section text must be a PlainText element, a KMarkdown element or a Paragraph struct if set.", + nameof(Text)); + + if (Accessory is not (null or ImageElementBuilder or ButtonElementBuilder)) + throw new ArgumentException( + "Section accessory must be an Image element or a Button element if set.", + nameof(Accessory)); + if (Mode != SectionAccessoryMode.Right && Accessory is ButtonElementBuilder) throw new InvalidOperationException("Button must be placed on the right"); @@ -285,27 +276,27 @@ public SectionModule Build() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(SectionModuleBuilder left, SectionModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(SectionModuleBuilder? left, SectionModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(SectionModuleBuilder left, SectionModuleBuilder right) - => !(left == right); + public static bool operator !=(SectionModuleBuilder? left, SectionModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is SectionModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is SectionModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(SectionModuleBuilder sectionModuleBuilder) + public bool Equals([NotNullWhen(true)] SectionModuleBuilder? sectionModuleBuilder) { if (sectionModuleBuilder is null) return false; @@ -317,4 +308,7 @@ public bool Equals(SectionModuleBuilder sectionModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as SectionModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/VideoModuleBuilder.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/VideoModuleBuilder.cs index ba3bc80e..026bb873 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/VideoModuleBuilder.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/Builders/VideoModuleBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Kook.Utils; namespace Kook; @@ -5,7 +6,7 @@ namespace Kook; /// /// Represents a video module builder for creating a . /// -public class VideoModuleBuilder : IModuleBuilder, IEquatable +public class VideoModuleBuilder : IModuleBuilder, IEquatable, IEquatable { /// public ModuleType Type => ModuleType.Video; @@ -22,10 +23,10 @@ public VideoModuleBuilder() /// /// The source URL of the video. /// The title of the video. - public VideoModuleBuilder(string source, string title = null) + public VideoModuleBuilder(string source, string? title = null) { - WithSource(source); - WithTitle(title); + Source = source; + Title = title; } /// @@ -34,7 +35,7 @@ public VideoModuleBuilder(string source, string title = null) /// /// The source URL of the video. /// - public string Source { get; set; } + public string? Source { get; set; } /// /// Gets or sets the title of the video. @@ -42,7 +43,7 @@ public VideoModuleBuilder(string source, string title = null) /// /// The title of the video. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// Sets the source URL of the video. @@ -53,7 +54,7 @@ public VideoModuleBuilder(string source, string title = null) /// /// The current builder. /// - public VideoModuleBuilder WithSource(string source) + public VideoModuleBuilder WithSource(string? source) { Source = source; return this; @@ -80,47 +81,57 @@ public VideoModuleBuilder WithTitle(string title) /// /// A representing the built video module object. /// - /// - /// does not include a protocol (neither HTTP nor HTTPS) + /// + /// The url is null. /// /// - /// cannot be null or empty + /// The url is empty. /// + /// + /// The url does not include a protocol (either HTTP or HTTPS). + /// + [MemberNotNull(nameof(Source))] public VideoModule Build() { - if (!UrlValidation.Validate(Source)) throw new ArgumentException("The link to a file cannot be null or empty.", nameof(Source)); + if (Source == null) + throw new ArgumentNullException(nameof(Source), "The source url cannot be null or empty."); + if (string.IsNullOrEmpty(Source)) + throw new ArgumentException("The source url cannot be null or empty.", nameof(Source)); + + UrlValidation.Validate(Source); return new VideoModule(Source, Title); } /// + [MemberNotNull(nameof(Source))] IModule IModuleBuilder.Build() => Build(); /// /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(VideoModuleBuilder left, VideoModuleBuilder right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(VideoModuleBuilder? left, VideoModuleBuilder? right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(VideoModuleBuilder left, VideoModuleBuilder right) - => !(left == right); + public static bool operator !=(VideoModuleBuilder? left, VideoModuleBuilder? right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is VideoModuleBuilder builder && Equals(builder); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is VideoModuleBuilder builder && Equals(builder); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(VideoModuleBuilder videoModuleBuilder) + public bool Equals([NotNullWhen(true)] VideoModuleBuilder? videoModuleBuilder) { if (videoModuleBuilder is null) return false; @@ -131,4 +142,7 @@ public bool Equals(VideoModuleBuilder videoModuleBuilder) /// public override int GetHashCode() => base.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModuleBuilder? moduleBuilder) => + Equals(moduleBuilder as VideoModuleBuilder); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContainerModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContainerModule.cs index ef1b4ec9..3c92798a 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContainerModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContainerModule.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,9 +8,12 @@ namespace Kook; /// Represents a container module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ContainerModule : IModule, IEquatable +public class ContainerModule : IModule, IEquatable, IEquatable { - internal ContainerModule(ImmutableArray elements) => Elements = elements; + internal ContainerModule(ImmutableArray elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.Container; @@ -28,28 +32,28 @@ public class ContainerModule : IModule, IEquatable /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ContainerModule left, ContainerModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ContainerModule left, ContainerModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ContainerModule left, ContainerModule right) - => !(left == right); + public static bool operator !=(ContainerModule left, ContainerModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ContainerModule containerModule && Equals(containerModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ContainerModule containerModule && Equals(containerModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ContainerModule containerModule) - => GetHashCode() == containerModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ContainerModule? containerModule) => + GetHashCode() == containerModule?.GetHashCode(); /// public override int GetHashCode() @@ -58,9 +62,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ Type.GetHashCode(); - foreach (ImageElement element in Elements) hash = (hash * 16777619) ^ element.GetHashCode(); - + foreach (ImageElement element in Elements) + hash = (hash * 16777619) ^ element.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as ContainerModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContextModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContextModule.cs index 5e5a0822..27b8ec1c 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContextModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ContextModule.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,9 +8,12 @@ namespace Kook; /// Represents a context module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ContextModule : IModule, IEquatable +public class ContextModule : IModule, IEquatable, IEquatable { - internal ContextModule(ImmutableArray elements) => Elements = elements; + internal ContextModule(ImmutableArray elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.Context; @@ -28,28 +32,28 @@ public class ContextModule : IModule, IEquatable /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(ContextModule left, ContextModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ContextModule left, ContextModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(ContextModule left, ContextModule right) - => !(left == right); + public static bool operator !=(ContextModule left, ContextModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ContextModule contextModule && Equals(contextModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ContextModule contextModule && Equals(contextModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ContextModule contextModule) - => GetHashCode() == contextModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ContextModule? contextModule) => + GetHashCode() == contextModule?.GetHashCode(); /// public override int GetHashCode() @@ -58,9 +62,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ Type.GetHashCode(); - foreach (IElement element in Elements) hash = (hash * 16777619) ^ element.GetHashCode(); - + foreach (IElement element in Elements) + hash = (hash * 16777619) ^ element.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as ContextModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/CountdownModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/CountdownModule.cs index 9b5a3dad..953a59c4 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/CountdownModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/CountdownModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,7 +7,7 @@ namespace Kook; /// Represents a countdown module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class CountdownModule : IModule, IEquatable +public class CountdownModule : IModule, IEquatable, IEquatable { internal CountdownModule(CountdownMode mode, DateTimeOffset endTime, DateTimeOffset? startTime = null) { @@ -51,8 +52,8 @@ internal CountdownModule(CountdownMode mode, DateTimeOffset endTime, DateTimeOff /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(CountdownModule left, CountdownModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(CountdownModule left, CountdownModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -60,23 +61,26 @@ internal CountdownModule(CountdownMode mode, DateTimeOffset endTime, DateTimeOff /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(CountdownModule left, CountdownModule right) - => !(left == right); + public static bool operator !=(CountdownModule left, CountdownModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is CountdownModule countdownModule && Equals(countdownModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is CountdownModule countdownModule && Equals(countdownModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(CountdownModule countdownModule) - => GetHashCode() == countdownModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] CountdownModule? countdownModule) => + GetHashCode() == countdownModule?.GetHashCode(); /// - public override int GetHashCode() - => (Type, EndTime, StartTime, Mode).GetHashCode(); + public override int GetHashCode() => + (Type, EndTime, StartTime, Mode).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as CountdownModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/DividerModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/DividerModule.cs index 0638c991..f4cfc7b5 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/DividerModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/DividerModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,7 +7,7 @@ namespace Kook; /// A divider module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class DividerModule : IModule, IEquatable +public class DividerModule : IModule, IEquatable, IEquatable { internal DividerModule() { @@ -21,30 +22,33 @@ internal DividerModule() /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(DividerModule left, DividerModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(DividerModule left, DividerModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(DividerModule left, DividerModule right) - => !(left == right); + public static bool operator !=(DividerModule left, DividerModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is DividerModule dividerModule && Equals(dividerModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is DividerModule dividerModule && Equals(dividerModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(DividerModule dividerModule) - => GetHashCode() == dividerModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] DividerModule? dividerModule) => + GetHashCode() == dividerModule?.GetHashCode(); /// - public override int GetHashCode() - => Type.GetHashCode(); + public override int GetHashCode() => + Type.GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as DividerModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs index 560a79eb..7709c7e4 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// A file module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class FileModule : IMediaModule, IEquatable +public class FileModule : IMediaModule, IEquatable, IEquatable { - internal FileModule(string source, string title) + internal FileModule(string source, string? title) { Source = source; Title = title; @@ -21,7 +22,7 @@ internal FileModule(string source, string title) public string Source { get; } /// - public string Title { get; } + public string? Title { get; } private string DebuggerDisplay => $"{Type}: {Title}"; @@ -31,8 +32,8 @@ internal FileModule(string source, string title) /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(FileModule left, FileModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(FileModule left, FileModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -40,23 +41,26 @@ internal FileModule(string source, string title) /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(FileModule left, FileModule right) - => !(left == right); + public static bool operator !=(FileModule left, FileModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is FileModule fileModule && Equals(fileModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is FileModule fileModule && Equals(fileModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(FileModule fileModule) - => GetHashCode() == fileModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] FileModule? fileModule) => + GetHashCode() == fileModule?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Source, Title).GetHashCode(); + public override int GetHashCode() => + (Type, Source, Title).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as FileModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/HeaderModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/HeaderModule.cs index b55fd216..70790e55 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/HeaderModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/HeaderModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,12 @@ namespace Kook; /// Represents a header module in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class HeaderModule : IModule, IEquatable +public class HeaderModule : IModule, IEquatable, IEquatable { - internal HeaderModule(PlainTextElement text) => Text = text; + internal HeaderModule(PlainTextElement? text) + { + Text = text; + } /// public ModuleType Type => ModuleType.Header; @@ -19,10 +23,10 @@ public class HeaderModule : IModule, IEquatable /// /// A representing the text of the header. /// - public PlainTextElement Text { get; } + public PlainTextElement? Text { get; } /// - public override string ToString() => Text.ToString(); + public override string? ToString() => Text?.ToString(); private string DebuggerDisplay => $"{Type}: {Text}"; @@ -30,28 +34,28 @@ public class HeaderModule : IModule, IEquatable /// Determines whether the specified is equal to the current . /// /// true if the specified is equal to the current ; otherwise, false. - public static bool operator ==(HeaderModule left, HeaderModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(HeaderModule left, HeaderModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . /// /// true if the specified is not equal to the current ; otherwise, false. - public static bool operator !=(HeaderModule left, HeaderModule right) - => !(left == right); + public static bool operator !=(HeaderModule left, HeaderModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is HeaderModule headerModule && Equals(headerModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is HeaderModule headerModule && Equals(headerModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(HeaderModule headerModule) - => GetHashCode() == headerModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] HeaderModule? headerModule) => + GetHashCode() == headerModule?.GetHashCode(); /// public override int GetHashCode() @@ -60,8 +64,11 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ Type.GetHashCode(); - hash = (hash * 16777619) ^ Text.GetHashCode(); + hash = (hash * 16777619) ^ (Text?.GetHashCode() ?? 0); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as HeaderModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/IMediaModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/IMediaModule.cs index 8d5a7bd7..d1b6482b 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/IMediaModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/IMediaModule.cs @@ -19,5 +19,5 @@ public interface IMediaModule : IModule /// /// A string representing the title of the media associated with this module. /// - string Title { get; } + string? Title { get; } } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ImageGroupModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ImageGroupModule.cs index a8cdaa0e..80340c0d 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ImageGroupModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/ImageGroupModule.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -7,9 +8,12 @@ namespace Kook; /// Represents an image group module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ImageGroupModule : IModule, IEquatable +public class ImageGroupModule : IModule, IEquatable, IEquatable { - internal ImageGroupModule(ImmutableArray elements) => Elements = elements; + internal ImageGroupModule(ImmutableArray elements) + { + Elements = elements; + } /// public ModuleType Type => ModuleType.ImageGroup; @@ -30,8 +34,8 @@ public class ImageGroupModule : IModule, IEquatable /// /// true if the specified is equal to the current ; /// - public static bool operator ==(ImageGroupModule left, ImageGroupModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(ImageGroupModule left, ImageGroupModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -39,21 +43,21 @@ public class ImageGroupModule : IModule, IEquatable /// /// true if the specified is not equal to the current ; /// - public static bool operator !=(ImageGroupModule left, ImageGroupModule right) - => !(left == right); + public static bool operator !=(ImageGroupModule left, ImageGroupModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is ImageGroupModule imageGroupModule && Equals(imageGroupModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is ImageGroupModule imageGroupModule && Equals(imageGroupModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(ImageGroupModule imageGroupModule) - => GetHashCode() == imageGroupModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] ImageGroupModule? imageGroupModule) => + GetHashCode() == imageGroupModule?.GetHashCode(); /// public override int GetHashCode() @@ -62,9 +66,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ Type.GetHashCode(); - foreach (ImageElement element in Elements) hash = (hash * 16777619) ^ element.GetHashCode(); - + foreach (ImageElement element in Elements) + hash = (hash * 16777619) ^ element.GetHashCode(); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as ImageGroupModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/InviteModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/InviteModule.cs index 575f27af..6381a2c7 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/InviteModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/InviteModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,12 @@ namespace Kook; /// An invite module that can be used in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class InviteModule : IModule, IEquatable +public class InviteModule : IModule, IEquatable, IEquatable { - internal InviteModule(string code) => Code = code; + internal InviteModule(string? code) + { + Code = code; + } /// public ModuleType Type => ModuleType.Invite; @@ -16,7 +20,7 @@ public class InviteModule : IModule, IEquatable /// /// Gets the invite code. /// - public string Code { get; } + public string? Code { get; } private string DebuggerDisplay => $"{Type}: {Code}"; @@ -26,8 +30,8 @@ public class InviteModule : IModule, IEquatable /// /// true if the specified is equal to the current ; /// - public static bool operator ==(InviteModule left, InviteModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(InviteModule left, InviteModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -35,23 +39,26 @@ public class InviteModule : IModule, IEquatable /// /// true if the specified is not equal to the current ; /// - public static bool operator !=(InviteModule left, InviteModule right) - => !(left == right); + public static bool operator !=(InviteModule left, InviteModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is InviteModule inviteModule && Equals(inviteModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is InviteModule inviteModule && Equals(inviteModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(InviteModule inviteModule) - => GetHashCode() == inviteModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] InviteModule? inviteModule) => + GetHashCode() == inviteModule?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Code).GetHashCode(); + public override int GetHashCode() => + (Type, Code).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as InviteModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionAccessoryMode.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionAccessoryMode.cs index 7f10cb5b..9d2de74b 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionAccessoryMode.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionAccessoryMode.cs @@ -1,15 +1,10 @@ namespace Kook; /// -/// Specifies the accessory position relative to the text element. +/// Specifies the accessory position relative to the text element. /// public enum SectionAccessoryMode { - /// - /// How the accessory is positioned relative to the text element is not specified. - /// - Unspecified, - /// /// The accessory is positioned to the left of the text element. /// diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionModule.cs index a8261ead..a91077be 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/SectionModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// Represents a section module in card. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class SectionModule : IModule, IEquatable +public class SectionModule : IModule, IEquatable, IEquatable { - internal SectionModule(SectionAccessoryMode mode, IElement text, IElement accessory = null) + internal SectionModule(SectionAccessoryMode? mode, IElement? text = null, IElement? accessory = null) { Mode = mode; Text = text; @@ -24,9 +25,8 @@ internal SectionModule(SectionAccessoryMode mode, IElement text, IElement access /// /// if the is to the left of , /// if the is to the right of , - /// if how the is positioned is not specified. /// - public SectionAccessoryMode Mode { get; } + public SectionAccessoryMode? Mode { get; } /// /// Gets the text of the section. @@ -34,7 +34,7 @@ internal SectionModule(SectionAccessoryMode mode, IElement text, IElement access /// /// An representing the text of the section. /// - public IElement Text { get; } + public IElement? Text { get; } /// /// Gets the accessory of the section. @@ -42,7 +42,7 @@ internal SectionModule(SectionAccessoryMode mode, IElement text, IElement access /// /// An representing the accessory of the section. /// - public IElement Accessory { get; } + public IElement? Accessory { get; } private string DebuggerDisplay => $"{Type}: {Text}{(Accessory is null ? string.Empty : $"{Mode} Accessory")}"; @@ -52,8 +52,8 @@ internal SectionModule(SectionAccessoryMode mode, IElement text, IElement access /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(SectionModule left, SectionModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(SectionModule left, SectionModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -61,21 +61,21 @@ internal SectionModule(SectionAccessoryMode mode, IElement text, IElement access /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(SectionModule left, SectionModule right) - => !(left == right); + public static bool operator !=(SectionModule left, SectionModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is SectionModule sectionModule && Equals(sectionModule); + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is SectionModule sectionModule && Equals(sectionModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(SectionModule sectionModule) - => GetHashCode() == sectionModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)] SectionModule? sectionModule) => + GetHashCode() == sectionModule?.GetHashCode(); /// public override int GetHashCode() @@ -84,9 +84,12 @@ public override int GetHashCode() { int hash = (int)2166136261; hash = (hash * 16777619) ^ (Type, Mode).GetHashCode(); - hash = (hash * 16777619) ^ Text.GetHashCode(); - hash = (hash * 16777619) ^ Accessory.GetHashCode(); + hash = (hash * 16777619) ^ (Text?.GetHashCode() ?? 0); + hash = (hash * 16777619) ^ (Accessory?.GetHashCode() ?? 0); return hash; } } + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as SectionModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/VideoModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/VideoModule.cs index 3725d36f..636e1652 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/VideoModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/VideoModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -6,9 +7,9 @@ namespace Kook; /// Represents a video module in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class VideoModule : IMediaModule, IEquatable +public class VideoModule : IMediaModule, IEquatable, IEquatable { - internal VideoModule(string source, string title) + internal VideoModule(string source, string? title) { Source = source; Title = title; @@ -21,7 +22,7 @@ internal VideoModule(string source, string title) public string Source { get; } /// - public string Title { get; } + public string? Title { get; } private string DebuggerDisplay => $"{Type}: {Title}"; @@ -31,8 +32,8 @@ internal VideoModule(string source, string title) /// /// true if the specified is equal to the current ; otherwise, false. /// - public static bool operator ==(VideoModule left, VideoModule right) - => left?.Equals(right) ?? right is null; + public static bool operator ==(VideoModule left, VideoModule right) => + left?.Equals(right) ?? right is null; /// /// Determines whether the specified is not equal to the current . @@ -40,23 +41,26 @@ internal VideoModule(string source, string title) /// /// true if the specified is not equal to the current ; otherwise, false. /// - public static bool operator !=(VideoModule left, VideoModule right) - => !(left == right); + public static bool operator !=(VideoModule left, VideoModule right) => + !(left == right); /// Determines whether the specified is equal to the current . /// If the object passes is an , will be called to compare the 2 instances. /// The object to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public override bool Equals(object obj) - => obj is VideoModule videoModule && Equals(videoModule); + public override bool Equals([NotNullWhen(true)]object? obj) => + obj is VideoModule videoModule && Equals(videoModule); /// Determines whether the specified is equal to the current . /// The to compare with the current . /// true if the specified is equal to the current ; otherwise, false. - public bool Equals(VideoModule videoModule) - => GetHashCode() == videoModule?.GetHashCode(); + public bool Equals([NotNullWhen(true)]VideoModule? videoModule) => + GetHashCode() == videoModule?.GetHashCode(); /// - public override int GetHashCode() - => (Type, Source, Title).GetHashCode(); + public override int GetHashCode() => + (Type, Source, Title).GetHashCode(); + + bool IEquatable.Equals([NotNullWhen(true)] IModule? module) => + Equals(module as VideoModule); } diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/BilibiliVideoEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/BilibiliVideoEmbed.cs index cac81353..fdf380d3 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/BilibiliVideoEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/BilibiliVideoEmbed.cs @@ -17,9 +17,14 @@ internal BilibiliVideoEmbed(string url, string originUrl, string bvNumber, strin } /// - public EmbedType Type => EmbedType.Link; + public EmbedType Type => EmbedType.BilibiliVideo; - /// + /// + /// Gets the URL of this embed. + /// + /// + /// A string that represents the URL of this embed. + /// public string Url { get; internal set; } /// diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/CardEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/CardEmbed.cs new file mode 100644 index 00000000..ffad295e --- /dev/null +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/CardEmbed.cs @@ -0,0 +1,20 @@ +namespace Kook; + +/// +/// Represents an embed in a message that +/// +public struct CardEmbed : IEmbed +{ + internal CardEmbed(ICard card) + { + Card = card; + } + + /// + /// Gets the cards in this embed. + /// + public ICard Card { get; internal set; } + + /// + public EmbedType Type => EmbedType.Card; +} diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/EmbedType.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/EmbedType.cs index 42910eaa..6c8ab362 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/EmbedType.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/EmbedType.cs @@ -23,7 +23,12 @@ public enum EmbedType /// /// Represents an embed that is a Bilibili video. /// - BilibiliVideo + BilibiliVideo, + + /// + /// Represents an embed that is a card. + /// + Card, // TODO: To be investigated } diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/IEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/IEmbed.cs index 569e1eef..9b1b94d7 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/IEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/IEmbed.cs @@ -13,12 +13,4 @@ public interface IEmbed /// A that represents the type of this embed. /// EmbedType Type { get; } - - /// - /// Gets the URL of this embed. - /// - /// - /// A string that represents the URL of this embed. - /// - string Url { get; } } diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/ImageEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/ImageEmbed.cs index e4e90fab..4c5e145f 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/ImageEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/ImageEmbed.cs @@ -12,9 +12,14 @@ internal ImageEmbed(string url, string originUrl) } /// - public EmbedType Type => EmbedType.Link; + public EmbedType Type => EmbedType.Image; - /// + /// + /// Gets the URL of this embed. + /// + /// + /// A string that represents the URL of this embed. + /// public string Url { get; internal set; } /// diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/LinkEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/LinkEmbed.cs index 22b0148f..91dc9b90 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/LinkEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/LinkEmbed.cs @@ -18,7 +18,12 @@ internal LinkEmbed(string url, string title, string description, string siteName /// public EmbedType Type => EmbedType.Link; - /// + /// + /// Gets the URL of this embed. + /// + /// + /// A string that represents the URL of this embed. + /// public string Url { get; internal set; } /// diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs index 795a59a1..9b1ae1ff 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs @@ -13,7 +13,6 @@ public struct NotImplementedEmbed : IEmbed internal NotImplementedEmbed(string rawType, JsonNode jsonNode) { RawType = rawType; - Url = null; JsonNode = jsonNode; } @@ -31,9 +30,6 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode) /// public string RawType { get; internal set; } - /// - public string Url { get; internal set; } - /// /// Gets the raw JSON of the embed. /// @@ -54,14 +50,15 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode) /// /// A representing the resolved embed. /// - public T Resolve(JsonSerializerOptions options = null) + public T? Resolve(JsonSerializerOptions? options = null) where T : IEmbed { options ??= new JsonSerializerOptions { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; - T embed = JsonNode.Deserialize(options); + T? embed = JsonNode.Deserialize(options); return embed; } diff --git a/src/Kook.Net.Core/Entities/Messages/FileAttachment.cs b/src/Kook.Net.Core/Entities/Messages/FileAttachment.cs index d098b391..b0a448a0 100644 --- a/src/Kook.Net.Core/Entities/Messages/FileAttachment.cs +++ b/src/Kook.Net.Core/Entities/Messages/FileAttachment.cs @@ -20,32 +20,32 @@ public struct FileAttachment : IDisposable public AttachmentType Type { get; private set; } /// - /// Gets the filename. + /// Gets the filename. /// public string FileName { get; private set; } /// /// Gets the stream containing the file content. /// - public Stream Stream { get; } + public Stream? Stream { get; } /// /// Gets the URI of the file. /// - public Uri Uri { get; internal set; } + public Uri? Uri { get; internal set; } /// /// Creates a file attachment from a stream. /// /// The stream to create the attachment from. - /// The name of the attachment. + /// The name of the attachment. /// The type of the attachment. - public FileAttachment(Stream stream, string fileName, AttachmentType type = AttachmentType.File) + public FileAttachment(Stream stream, string filename, AttachmentType type = AttachmentType.File) { _isDisposed = false; Mode = CreateAttachmentMode.Stream; Type = type; - FileName = fileName; + FileName = filename; Stream = stream; try { @@ -67,7 +67,7 @@ public FileAttachment(Stream stream, string fileName, AttachmentType type = Atta /// . /// /// The path to the file. - /// The name of the attachment. + /// The name of the attachment. /// The type of the attachment. /// /// is a zero-length string, contains only white space, or contains one or @@ -96,13 +96,13 @@ public FileAttachment(Stream stream, string fileName, AttachmentType type = Atta /// /// An I/O error occurred while opening the file. /// - public FileAttachment(string path, string fileName = null, AttachmentType type = AttachmentType.File) + public FileAttachment(string path, string? filename = null, AttachmentType type = AttachmentType.File) { _isDisposed = false; Mode = CreateAttachmentMode.FilePath; Type = type; Stream = File.OpenRead(path); - FileName = fileName ?? Path.GetFileName(path); + FileName = filename ?? Path.GetFileName(path); Uri = null; } @@ -115,18 +115,18 @@ public FileAttachment(string path, string fileName = null, AttachmentType type = /// Under this circumstance, please create asset in advance. /// /// The URI of the file. - /// The name of the attachment. + /// The name of the attachment. /// The type of the attachment. /// The URI provided is not an asset on the KOOK OSS. /// The URI provided is blank. /// - public FileAttachment(Uri uri, string fileName, AttachmentType type = AttachmentType.File) + public FileAttachment(Uri uri, string filename, AttachmentType type = AttachmentType.File) { _isDisposed = false; Mode = CreateAttachmentMode.AssetUri; Type = type; Stream = null; - FileName = fileName; + FileName = filename; Uri = uri; } diff --git a/src/Kook.Net.Core/Entities/Messages/IAttachment.cs b/src/Kook.Net.Core/Entities/Messages/IAttachment.cs index 91f74cb7..a640c9d6 100644 --- a/src/Kook.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Kook.Net.Core/Entities/Messages/IAttachment.cs @@ -27,7 +27,7 @@ public interface IAttachment /// /// A string containing the full filename of this attachment. /// - string Filename { get; } + string? Filename { get; } /// /// Gets the file size of the attachment. @@ -45,7 +45,7 @@ public interface IAttachment /// A string representing the file type of the attachment; /// null if the file type is unknown or not applicable. /// - string FileType { get; } + string? FileType { get; } /// /// Gets the duration of the attachment. diff --git a/src/Kook.Net.Core/Entities/Messages/IMessage.cs b/src/Kook.Net.Core/Entities/Messages/IMessage.cs index f8ce2c8f..6b597a65 100644 --- a/src/Kook.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Kook.Net.Core/Entities/Messages/IMessage.cs @@ -92,7 +92,7 @@ public interface IMessage : IEntity, IDeletable /// /// true if this message mentioned everyone; otherwise false. /// - bool? MentionedEveryone { get; } + bool MentionedEveryone { get; } /// /// Gets the value that indicates whether this message mentioned online users. @@ -100,7 +100,7 @@ public interface IMessage : IEntity, IDeletable /// /// true if this message mentioned online users; otherwise false. /// - bool? MentionedHere { get; } + bool MentionedHere { get; } /// /// Gets all tags included in this message's content. @@ -157,7 +157,7 @@ public interface IMessage : IEntity, IDeletable /// A task that represents the asynchronous operation for adding a reaction to this message. /// /// - Task AddReactionAsync(IEmote emote, RequestOptions options = null); + Task AddReactionAsync(IEmote emote, RequestOptions? options = null); /// /// Removes a reaction from message. @@ -169,7 +169,7 @@ public interface IMessage : IEntity, IDeletable /// A task that represents the asynchronous operation for removing a reaction to this message. /// /// - Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); + Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null); /// /// Removes a reaction from message. @@ -181,7 +181,7 @@ public interface IMessage : IEntity, IDeletable /// A task that represents the asynchronous operation for removing a reaction to this message. /// /// - Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null); + Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null); /// /// Gets all users that reacted to a message with a given emote. @@ -191,7 +191,7 @@ public interface IMessage : IEntity, IDeletable /// /// Collection of users. /// - Task> GetReactionUsersAsync(IEmote emote, RequestOptions options = null); + Task> GetReactionUsersAsync(IEmote emote, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Messages/IQuote.cs b/src/Kook.Net.Core/Entities/Messages/IQuote.cs index bd8fdfbe..16487868 100644 --- a/src/Kook.Net.Core/Entities/Messages/IQuote.cs +++ b/src/Kook.Net.Core/Entities/Messages/IQuote.cs @@ -3,37 +3,10 @@ namespace Kook; /// /// Represents a generic message quote. /// -public interface IQuote : IEntity +public interface IQuote { /// /// Gets the identifier of the message this quote refers to. /// Guid QuotedMessageId { get; } - - /// - /// Gets the type of the message this quote refers to. - /// - MessageType Type { get; } - - /// - /// Gets the content of the message this quote refers to. - /// - /// - /// A string that contains the body of the message; - /// note that this field may be empty or the original code if the message is not a text based message. - /// - string Content { get; } - - /// - /// Gets the time this message was sent. - /// - /// - /// Time of when the message was sent. - /// - DateTimeOffset CreateAt { get; } - - /// - /// Gets the author of this message. - /// - IUser Author { get; } } diff --git a/src/Kook.Net.Core/Entities/Messages/ITag.cs b/src/Kook.Net.Core/Entities/Messages/ITag.cs index 460138dd..e563779a 100644 --- a/src/Kook.Net.Core/Entities/Messages/ITag.cs +++ b/src/Kook.Net.Core/Entities/Messages/ITag.cs @@ -29,5 +29,5 @@ public interface ITag /// /// Gets the value of the tag. /// - object Value { get; } + object? Value { get; } } diff --git a/src/Kook.Net.Core/Entities/Messages/IUserMessage.cs b/src/Kook.Net.Core/Entities/Messages/IUserMessage.cs index 792a526c..78493815 100644 --- a/src/Kook.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Kook.Net.Core/Entities/Messages/IUserMessage.cs @@ -11,7 +11,7 @@ public interface IUserMessage : IMessage /// /// The message quote. /// - IQuote Quote { get; } + IQuote? Quote { get; } /// /// Modifies this message. @@ -25,7 +25,7 @@ public interface IUserMessage : IMessage /// /// A task that represents the asynchronous modification operation. /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); /// /// Transforms this message's text into a human-readable form by resolving its tags. diff --git a/src/Kook.Net.Core/Entities/Messages/MessageProperties.cs b/src/Kook.Net.Core/Entities/Messages/MessageProperties.cs index da7a371f..8480e221 100644 --- a/src/Kook.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Kook.Net.Core/Entities/Messages/MessageProperties.cs @@ -12,20 +12,20 @@ public class MessageProperties /// /// This must be less than the constant defined by . /// - public string Content { get; set; } + public string? Content { get; set; } /// /// Gets or sets the cards of the message. /// - public IEnumerable Cards { get; set; } + public IEnumerable? Cards { get; set; } /// /// Gets or sets the quote of the message. /// - public IQuote Quote { get; set; } + public IQuote? Quote { get; set; } /// /// Gets or sets the only user that can see this message. /// - public IUser EphemeralUser { get; set; } + public IUser? EphemeralUser { get; set; } } diff --git a/src/Kook.Net.Core/Entities/Messages/MessageReference.cs b/src/Kook.Net.Core/Entities/Messages/MessageReference.cs new file mode 100644 index 00000000..343e345e --- /dev/null +++ b/src/Kook.Net.Core/Entities/Messages/MessageReference.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace Kook; + +/// +/// Represents a message reference. +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class MessageReference : IQuote +{ + /// + /// Gets an empty quote whose quoted message is null. + /// + /// + /// Used to delete a quote when modifying a message. + /// + public static MessageReference Empty => new(Guid.Empty); + + /// + /// Creates a new instance of with the specified quoted message identifier. + /// + /// + /// The identifier of the message that will be quoted. + /// If , the quote will be empty. + /// + public MessageReference(Guid quotedMessageId) + { + QuotedMessageId = quotedMessageId; + } + + /// + public Guid QuotedMessageId { get; } + + private string DebuggerDisplay => $"Quote: {QuotedMessageId}"; +} diff --git a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/ImageAnimationPokeResource.cs b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/ImageAnimationPokeResource.cs index 4438497c..ebeb4ada 100644 --- a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/ImageAnimationPokeResource.cs +++ b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/ImageAnimationPokeResource.cs @@ -5,7 +5,8 @@ namespace Kook; /// public struct ImageAnimationPokeResource : IPokeResource { - internal ImageAnimationPokeResource(IReadOnlyDictionary resources, TimeSpan duration, int width, int height, double percent) + internal ImageAnimationPokeResource(IReadOnlyDictionary resources, + TimeSpan duration, int width, int height, decimal percent) { Resources = resources; Duration = duration; @@ -23,7 +24,7 @@ internal ImageAnimationPokeResource(IReadOnlyDictionary resource public IReadOnlyDictionary Resources { get; internal set; } /// - /// Gets how long this animation animation should last filling the full screen. + /// Gets how long this animation should last filling the full screen. /// public TimeSpan Duration { get; internal set; } @@ -40,5 +41,5 @@ internal ImageAnimationPokeResource(IReadOnlyDictionary resource /// /// // TODO: To be documented. /// - public double Percent { get; internal set; } + public decimal Percent { get; internal set; } } diff --git a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs index 6f6a0d37..861e8ed1 100644 --- a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs +++ b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs @@ -50,14 +50,15 @@ internal NotImplementedPokeResource(string rawType, JsonNode jsonNode) /// /// A representing the resolved embed. /// - public T Resolve(JsonSerializerOptions options = null) + public T? Resolve(JsonSerializerOptions? options = null) where T : IPokeResource { options ??= new JsonSerializerOptions { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; - T pokeResource = JsonNode.Deserialize(options); + T? pokeResource = JsonNode.Deserialize(options); return pokeResource; } diff --git a/src/Kook.Net.Core/Entities/Messages/Quote.cs b/src/Kook.Net.Core/Entities/Messages/Quote.cs index 12b70642..8fbb6362 100644 --- a/src/Kook.Net.Core/Entities/Messages/Quote.cs +++ b/src/Kook.Net.Core/Entities/Messages/Quote.cs @@ -2,47 +2,44 @@ namespace Kook; -/// +/// +/// Represents a quoted message. +/// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class Quote : IQuote { - /// - public string Id { get; } - /// public Guid QuotedMessageId { get; } - /// + /// + /// Gets the type of the message this quote refers to. + /// public MessageType Type { get; } - /// + /// + /// Gets the content of the message this quote refers to. + /// + /// + /// A string that contains the body of the message; + /// note that this field may be empty or the original code if the message is not a text based message. + /// public string Content { get; } - /// - public DateTimeOffset CreateAt { get; } - - /// - public IUser Author { get; } - /// - /// Gets an empty quote whose quoted message is null. + /// Gets the time this message was sent. /// - /// - /// Used to delete a quote when modifying a message. - /// - public static Quote Empty => new(Guid.Empty); + /// + /// Time of when the message was sent. + /// + public DateTimeOffset CreateAt { get; } /// - /// Initializes a new instance of the class. + /// Gets the author of this message. /// - /// - /// The quoted message identifier. - /// - public Quote(Guid quotedMessageId) => QuotedMessageId = quotedMessageId; + public IUser Author { get; } - internal Quote(string id, Guid quotedMessageId, MessageType type, string content, DateTimeOffset createAt, IUser author) + internal Quote(Guid quotedMessageId, MessageType type, string content, DateTimeOffset createAt, IUser author) { - Id = id; QuotedMessageId = quotedMessageId; Type = type; Content = content; @@ -50,8 +47,9 @@ internal Quote(string id, Guid quotedMessageId, MessageType type, string content Author = author; } - internal static Quote Create(string id, Guid quotedMessageId, MessageType type, string content, DateTimeOffset createAt, IUser author) => - new(id, quotedMessageId, type, content, createAt, author); + internal static Quote Create(Guid quotedMessageId, MessageType type, + string content, DateTimeOffset createAt, IUser author) => + new(quotedMessageId, type, content, createAt, author); - private string DebuggerDisplay => $"{Author}: {Content} ({Id})"; + private string DebuggerDisplay => $"{Author}: {Content} {QuotedMessageId:P}"; } diff --git a/src/Kook.Net.Core/Entities/Messages/Tag.cs b/src/Kook.Net.Core/Entities/Messages/Tag.cs index 3b7c4e59..b2d5459f 100644 --- a/src/Kook.Net.Core/Entities/Messages/Tag.cs +++ b/src/Kook.Net.Core/Entities/Messages/Tag.cs @@ -5,7 +5,7 @@ namespace Kook; /// /// Represents a tag found in . /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class Tag : ITag { /// @@ -28,9 +28,9 @@ public class Tag : ITag /// this property returns the same entity as for convenience. /// because there is no actual entities representing a group of online users. /// - public T Value { get; } + public T? Value { get; } - internal Tag(TagType type, int index, int length, dynamic key, T value) + internal Tag(TagType type, int index, int length, dynamic key, T? value) { Type = type; Index = index; @@ -45,5 +45,5 @@ internal Tag(TagType type, int index, int length, dynamic key, T value) public override string ToString() => DebuggerDisplay; /// - object ITag.Value => Value; + object? ITag.Value => Value; } diff --git a/src/Kook.Net.Core/Entities/Messages/TagHandling.cs b/src/Kook.Net.Core/Entities/Messages/TagHandling.cs index 3fa812a7..cd27536f 100644 --- a/src/Kook.Net.Core/Entities/Messages/TagHandling.cs +++ b/src/Kook.Net.Core/Entities/Messages/TagHandling.cs @@ -7,38 +7,38 @@ namespace Kook; /// public enum TagHandling { - /// - /// Tag handling is ignored (e.g. <@53905483156684800> -> <@53905483156684800>). + /// + /// Tag handling is ignored. (e.g. (met)2810246202(met) -> (met)2810246202(met)) /// Ignore = 0, - /// - /// Removes the tag entirely. + /// + /// Removes the tag entirely. /// Remove, - /// - /// Resolves to username (e.g. <@53905483156684800> -> @Voltana). + /// + /// Resolves to username (e.g. (met)2810246202(met) -> @Someone). /// Name, - /// - /// Resolves to username without mention prefix (e.g. <@53905483156684800> -> Voltana). + /// + /// Resolves to username without mention prefix (e.g. (met)2810246202(met) -> Someone). /// NameNoPrefix, - /// - /// Resolves to username with identify number value. (e.g. <@53905483156684800> -> @Voltana#8252). + /// + /// Resolves to username with identify number value (e.g. (met)2810246202(met) -> @Someone#1234). /// FullName, - /// - /// Resolves to username with identify number value without mention prefix. (e.g. <@53905483156684800> -> Voltana#8252). + /// + /// Resolves to username with identify number value without mention prefix (e.g. (met)2810246202(met) -> Someone#1234). /// FullNameNoPrefix, - /// - /// Sanitizes the tag (e.g. <@53905483156684800> -> <@53905483156684800> (w/ nbsp)). + /// + /// Sanitizes the tag. (e.g. (met)2810246202(met) -> (met)2810246202(met) (an nbsp is inserted before the key)). /// Sanitize } diff --git a/src/Kook.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Kook.Net.Core/Entities/Permissions/ChannelPermissions.cs index ccd4b952..a1719dc9 100644 --- a/src/Kook.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Kook.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -93,7 +93,10 @@ public static ChannelPermissions All(IChannel channel) => public bool ShareScreen => Permissions.GetValue(RawValue, ChannelPermission.ShareScreen); /// Creates a new with the provided packed value. - public ChannelPermissions(ulong rawValue) => RawValue = rawValue; + public ChannelPermissions(ulong rawValue) + { + RawValue = rawValue; + } private ChannelPermissions(ulong initialValue, bool? createInvites = null, @@ -184,8 +187,8 @@ public ChannelPermissions Modify( bool? deafenMembers = null, bool? muteMembers = null, bool? playSoundtrack = null, - bool? shareScreen = null) - => new(RawValue, + bool? shareScreen = null) => + new(RawValue, createInvites, manageChannels, manageRoles, @@ -220,14 +223,15 @@ public ChannelPermissions Modify( /// A containing flags. Empty if none are enabled. public List ToList() { - List perms = new(); + List perms = []; // bitwise operations on raw value // each of the ChannelPermissions increments by 2^i from 0 to MaxBits for (byte i = 0; i < Permissions.MaxBits; i++) { ulong flag = (ulong)1 << i; - if ((RawValue & flag) != 0) perms.Add((ChannelPermission)flag); + if ((RawValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; diff --git a/src/Kook.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Kook.Net.Core/Entities/Permissions/GuildPermission.cs index 61c8b513..ab9c08c6 100644 --- a/src/Kook.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Kook.Net.Core/Entities/Permissions/GuildPermission.cs @@ -17,7 +17,7 @@ public enum GuildPermission : uint ManageGuild = 1 << 1, /// - /// Allows for viewing of audit logs. + /// Allows for viewing of audit logs. /// ViewAuditLog = 1 << 2, diff --git a/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs index 3032bf11..a61e51c2 100644 --- a/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -5,7 +5,7 @@ namespace Kook; /// /// Represents a set of permissions for a guild. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct GuildPermissions { /// Gets a blank that grants no permissions. @@ -105,10 +105,16 @@ public struct GuildPermissions public bool ShareScreen => Permissions.GetValue(RawValue, GuildPermission.ShareScreen); /// Creates a new with the provided packed value. - public GuildPermissions(ulong rawValue) => RawValue = rawValue; + public GuildPermissions(ulong rawValue) + { + RawValue = rawValue; + } /// Creates a new with the provided packed value after converting to ulong. - public GuildPermissions(string rawValue) => RawValue = ulong.Parse(rawValue); + public GuildPermissions(string rawValue) + { + RawValue = ulong.Parse(rawValue); + } private GuildPermissions(ulong initialValue, bool? administrator = null, @@ -245,8 +251,8 @@ public GuildPermissions Modify( bool? muteMembers = null, bool? manageNicknames = null, bool? playSoundtrack = null, - bool? shareScreen = null) - => new(RawValue, administrator, manageGuild, viewAuditLog, createInvites, manageInvites, + bool? shareScreen = null) => + new(RawValue, administrator, manageGuild, viewAuditLog, createInvites, manageInvites, manageChannels, kickMembers, banMembers, manageEmojis, changeNickname, manageRoles, viewChannel, sendMessages, manageMessages, attachFiles, connect, manageVoice, mentionEveryone, addReactions, followReactions, passiveConnect, onlyPushToTalk, useVoiceActivity, speak, deafenMembers, muteMembers, @@ -267,14 +273,15 @@ public GuildPermissions Modify( /// A containing flags. Empty if none are enabled. public List ToList() { - List perms = new(); + List perms = []; // bitwise operations on raw value // each of the GuildPermissions increments by 2^i from 0 to MaxBits for (byte i = 0; i < Permissions.MaxBits; i++) { ulong flag = (ulong)1 << i; - if ((RawValue & flag) != 0) perms.Add((GuildPermission)flag); + if ((RawValue & flag) != 0) + perms.Add((GuildPermission)flag); } return perms; @@ -284,9 +291,13 @@ internal void Ensure(GuildPermission permissions) { if (!Has(permissions)) { - IEnumerable vals = Enum.GetValues(typeof(GuildPermission)).Cast(); + IEnumerable vals = Enum + .GetValues(typeof(GuildPermission)) + .Cast(); ulong currentValues = RawValue; - IEnumerable missingValues = vals.Where(x => permissions.HasFlag(x) && !Permissions.GetValue(currentValues, x)); + IEnumerable missingValues = vals + .Where(x => permissions.HasFlag(x) && !Permissions.GetValue(currentValues, x)) + .ToList(); throw new InvalidOperationException( $"Missing required guild permission{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); diff --git a/src/Kook.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Kook.Net.Core/Entities/Permissions/OverwritePermissions.cs index 3fd2c91d..1468d4e3 100644 --- a/src/Kook.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Kook.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -5,7 +5,7 @@ namespace Kook; /// /// Represents a container for a series of overwrite permissions. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct OverwritePermissions { /// @@ -17,15 +17,15 @@ public struct OverwritePermissions /// Gets a that grants all permissions for the given channel. /// /// Unknown channel type. - public static OverwritePermissions AllowAll(IChannel channel) - => new(ChannelPermissions.All(channel).RawValue, 0); + public static OverwritePermissions AllowAll(IChannel channel) => + new(ChannelPermissions.All(channel).RawValue, 0); /// /// Gets a that denies all permissions for the given channel. /// /// Unknown channel type. - public static OverwritePermissions DenyAll(IChannel channel) - => new(0, ChannelPermissions.All(channel).RawValue); + public static OverwritePermissions DenyAll(IChannel channel) => + new(0, ChannelPermissions.All(channel).RawValue); /// /// Gets a packed value representing all the allowed permissions in this . @@ -198,8 +198,8 @@ public OverwritePermissions Modify( PermValue? deafenMembers = null, PermValue? muteMembers = null, PermValue? playSoundtrack = null, - PermValue? shareScreen = null) - => new(AllowValue, DenyValue, createInvites, manageChannels, manageRoles, viewChannel, + PermValue? shareScreen = null) => + new(AllowValue, DenyValue, createInvites, manageChannels, manageRoles, viewChannel, sendMessages, manageMessages, attachFiles, connect, manageVoice, mentionEveryone, addReactions, passiveConnect, useVoiceActivity, speak, deafenMembers, muteMembers, playSoundtrack, shareScreen); @@ -209,12 +209,13 @@ public OverwritePermissions Modify( /// A of all allowed flags. If none, the list will be empty. public List ToAllowList() { - List perms = new(); + List perms = []; for (byte i = 0; i < Permissions.MaxBits; i++) { // first operand must be long or ulong to shift >31 bits ulong flag = (ulong)1 << i; - if ((AllowValue & flag) != 0) perms.Add((ChannelPermission)flag); + if ((AllowValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; @@ -230,7 +231,8 @@ public List ToDenyList() for (byte i = 0; i < Permissions.MaxBits; i++) { ulong flag = (ulong)1 << i; - if ((DenyValue & flag) != 0) perms.Add((ChannelPermission)flag); + if ((DenyValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; diff --git a/src/Kook.Net.Core/Entities/Roles/AlphaColor.cs b/src/Kook.Net.Core/Entities/Roles/AlphaColor.cs index 08b74254..4db51c31 100644 --- a/src/Kook.Net.Core/Entities/Roles/AlphaColor.cs +++ b/src/Kook.Net.Core/Entities/Roles/AlphaColor.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using StandardColor = System.Drawing.Color; namespace Kook; @@ -6,8 +7,8 @@ namespace Kook; /// /// Represents a with an alpha channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public struct AlphaColor +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public readonly struct AlphaColor { /// Gets the max decimal value of an color with an alpha channel. public const ulong MaxDecimalValue = 0xFFFFFFFF; @@ -111,16 +112,16 @@ public AlphaColor(byte r, byte g, byte b, byte a) /// The argument value is not between 0 to 255. public AlphaColor(int r, int g, int b, int a) { - if (r < 0 || r > 255) + if (r is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]."); - if (g < 0 || g > 255) + if (g is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]."); - if (b < 0 || b > 255) + if (b is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]."); - if (a < 0 || a > 255) + if (a is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(a), "Value must be within [0,255]."); RawValue = ((ulong)(uint)r << 24) @@ -146,16 +147,16 @@ public AlphaColor(int r, int g, int b, int a) /// The argument value is not between 0 to 1. public AlphaColor(float r, float g, float b, float a) { - if (r < 0.0f || r > 1.0f) + if (r is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]."); - if (g < 0.0f || g > 1.0f) + if (g is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]."); - if (b < 0.0f || b > 1.0f) + if (b is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]."); - if (a < 0.0f || a > 1.0f) + if (a is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(a), "Value must be within [0,1]."); RawValue = ((uint)(r * 255.0f) << 24) @@ -168,35 +169,35 @@ public AlphaColor(float r, float g, float b, float a) /// Determines whether the specified is equal to this instance. /// /// true if the specified is equal to this instance; otherwise, false . - public static bool operator ==(AlphaColor lhs, AlphaColor rhs) - => lhs.RawValue == rhs.RawValue; + public static bool operator ==(AlphaColor lhs, AlphaColor rhs) => + lhs.RawValue == rhs.RawValue; /// /// Determines whether the specified is not equal to this instance. /// /// true if the specified is not equal to this instance; otherwise, false . - public static bool operator !=(AlphaColor lhs, AlphaColor rhs) - => lhs.RawValue != rhs.RawValue; + public static bool operator !=(AlphaColor lhs, AlphaColor rhs) => + lhs.RawValue != rhs.RawValue; /// /// Converts the given raw value of to a . /// /// The raw value of the color. /// The that represents the given raw value. - public static implicit operator AlphaColor(ulong rawValue) - => new(rawValue); + public static implicit operator AlphaColor(ulong rawValue) => + new(rawValue); /// /// Converts the given to its raw value of . /// /// The to convert. /// The raw value of the given . - public static implicit operator ulong(AlphaColor color) - => color.RawValue; + public static implicit operator ulong(AlphaColor color) => + color.RawValue; /// - public override bool Equals(object obj) - => obj is AlphaColor c && RawValue == c.RawValue; + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is AlphaColor c && RawValue == c.RawValue; /// public override int GetHashCode() => RawValue.GetHashCode(); @@ -206,8 +207,8 @@ public override bool Equals(object obj) /// /// The Kook.Net-defined to convert. /// The Kook.Net-defined that represents the given Kook.Net-defined . - public static implicit operator AlphaColor(Color color) - => new(((ulong)color.RawValue << 8) | 0xFF); + public static implicit operator AlphaColor(Color color) => + new(((ulong)color.RawValue << 8) | 0xFF); /// /// Converts the given Kook.Net-defined to a Kook.Net-defined . @@ -226,16 +227,16 @@ public static implicit operator AlphaColor(Color color) /// /// The Kook.Net-defined to convert. /// The .NET standard that represents the given Kook.Net-defined . - public static implicit operator StandardColor(AlphaColor color) - => StandardColor.FromArgb(color.A, color.R, color.G, color.B); + public static implicit operator StandardColor(AlphaColor color) => + StandardColor.FromArgb(color.A, color.R, color.G, color.B); /// /// Converts the given .NET standard to a Kook.Net-defined . /// /// The .NET standard to convert. /// The Kook.Net-defined that represents the given .NET standard . - public static explicit operator AlphaColor(StandardColor color) - => new(color.R, color.G, color.B, color.A); + public static explicit operator AlphaColor(StandardColor color) => + new(color.R, color.G, color.B, color.A); /// /// Gets the hexadecimal representation of the color (e.g. #000cccff). diff --git a/src/Kook.Net.Core/Entities/Roles/Color.cs b/src/Kook.Net.Core/Entities/Roles/Color.cs index 58a8363c..1ee5de01 100644 --- a/src/Kook.Net.Core/Entities/Roles/Color.cs +++ b/src/Kook.Net.Core/Entities/Roles/Color.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using StandardColor = System.Drawing.Color; namespace Kook; @@ -6,8 +7,8 @@ namespace Kook; /// /// Represents a color used in Kook. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public struct Color +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public readonly struct Color { /// Gets the max decimal value of color. public const uint MaxDecimalValue = 0xFFFFFF; @@ -173,13 +174,13 @@ public Color(byte r, byte g, byte b) /// The argument value is not between 0 to 255. public Color(int r, int g, int b) { - if (r < 0 || r > 255) + if (r is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]."); - if (g < 0 || g > 255) + if (g is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]."); - if (b < 0 || b > 255) + if (b is < 0 or > 255) throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]."); RawValue = ((uint)r << 16) @@ -203,13 +204,13 @@ public Color(int r, int g, int b) /// The argument value is not between 0 to 1. public Color(float r, float g, float b) { - if (r < 0.0f || r > 1.0f) + if (r is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]."); - if (g < 0.0f || g > 1.0f) + if (g is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]."); - if (b < 0.0f || b > 1.0f) + if (b is < 0.0f or > 1.0f) throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]."); RawValue = ((uint)(r * 255.0f) << 16) @@ -221,35 +222,35 @@ public Color(float r, float g, float b) /// Determines whether the specified is equal to this instance. /// /// true if the specified is equal to this instance; otherwise, false . - public static bool operator ==(Color lhs, Color rhs) - => lhs.RawValue == rhs.RawValue; + public static bool operator ==(Color lhs, Color rhs) => + lhs.RawValue == rhs.RawValue; /// /// Determines whether the specified is not equal to this instance. /// /// true if the specified is not equal to this instance; otherwise, false . - public static bool operator !=(Color lhs, Color rhs) - => lhs.RawValue != rhs.RawValue; + public static bool operator !=(Color lhs, Color rhs) => + lhs.RawValue != rhs.RawValue; /// /// Converts the given raw value of to a . /// /// The raw value of the color. /// The that represents the given raw value. - public static implicit operator Color(uint rawValue) - => new(rawValue); + public static implicit operator Color(uint rawValue) => + new(rawValue); /// /// Converts the given to its raw value of . /// /// The to convert. /// The raw value of the given . - public static implicit operator uint(Color color) - => color.RawValue; + public static implicit operator uint(Color color) => + color.RawValue; /// - public override bool Equals(object obj) - => obj is Color c && RawValue == c.RawValue; + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is Color c && RawValue == c.RawValue; /// public override int GetHashCode() => RawValue.GetHashCode(); @@ -259,16 +260,16 @@ public override bool Equals(object obj) /// /// The Kook.Net-defined to convert. /// The .NET standard that represents the given Kook.Net-defined . - public static implicit operator StandardColor(Color color) - => StandardColor.FromArgb((int)color.RawValue); + public static implicit operator StandardColor(Color color) => + StandardColor.FromArgb((int)color.RawValue); /// /// Converts the given .NET standard to a Kook.Net-defined . /// /// The .NET standard to convert. /// The Kook.Net-defined that represents the given .NET standard . - public static explicit operator Color(StandardColor color) - => new(((uint)color.ToArgb() << 8) >> 8); + public static explicit operator Color(StandardColor color) => + new(((uint)color.ToArgb() << 8) >> 8); /// /// Gets the hexadecimal representation of the color (e.g. #000ccc). diff --git a/src/Kook.Net.Core/Entities/Roles/GradientColor.cs b/src/Kook.Net.Core/Entities/Roles/GradientColor.cs index 541cc8a6..4d2a8dea 100644 --- a/src/Kook.Net.Core/Entities/Roles/GradientColor.cs +++ b/src/Kook.Net.Core/Entities/Roles/GradientColor.cs @@ -5,8 +5,8 @@ namespace Kook; /// /// Represents a gradient color. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public struct GradientColor +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public readonly struct GradientColor { /// /// Initializes a new instance of . diff --git a/src/Kook.Net.Core/Entities/Roles/IRole.cs b/src/Kook.Net.Core/Entities/Roles/IRole.cs index ae0ba376..2ae310bd 100644 --- a/src/Kook.Net.Core/Entities/Roles/IRole.cs +++ b/src/Kook.Net.Core/Entities/Roles/IRole.cs @@ -27,7 +27,7 @@ public interface IRole : IEntity, IDeletable, IMentionable, IComparable /// A representing the type of this role. /// - RoleType? Type { get; } + RoleType Type { get; } /// /// Gets the color given to users of this role. @@ -98,15 +98,15 @@ public interface IRole : IEntity, IDeletable, IMentionable, IComparable /// A task that represents the asynchronous modification operation. /// - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions? options = null); /// /// Gets a collection of users with this role. /// - /// The options to be used when sending the request. /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. /// /// Paged collection of users with this role. /// - IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Roles/RoleProperties.cs b/src/Kook.Net.Core/Entities/Roles/RoleProperties.cs index 614929c0..46739db5 100644 --- a/src/Kook.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Kook.Net.Core/Entities/Roles/RoleProperties.cs @@ -12,7 +12,7 @@ public class RoleProperties /// /// This value may not be set if the role is an @everyone role. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the color of the role. @@ -23,7 +23,7 @@ public class RoleProperties public Color? Color { get; set; } /// - /// Gets or sets whether or not this role should be displayed independently in the user list. + /// Gets or sets whether this role should be displayed independently in the user list. /// /// /// This value may not be set if the role is an @everyone role. diff --git a/src/Kook.Net.Core/Entities/Users/BoostSubscriptionMetadata.cs b/src/Kook.Net.Core/Entities/Users/BoostSubscriptionMetadata.cs index bb8673d7..4eedee46 100644 --- a/src/Kook.Net.Core/Entities/Users/BoostSubscriptionMetadata.cs +++ b/src/Kook.Net.Core/Entities/Users/BoostSubscriptionMetadata.cs @@ -1,7 +1,7 @@ namespace Kook; /// -/// A meta data containing boost subscription information. +/// A metadata containing boost subscription information. /// public class BoostSubscriptionMetadata { diff --git a/src/Kook.Net.Core/Entities/Users/ClientType.cs b/src/Kook.Net.Core/Entities/Users/ClientType.cs index f024c1fa..23851f7b 100644 --- a/src/Kook.Net.Core/Entities/Users/ClientType.cs +++ b/src/Kook.Net.Core/Entities/Users/ClientType.cs @@ -18,5 +18,6 @@ public enum ClientType /// /// The user is active using the iOS application. /// + // ReSharper disable once InconsistentNaming iOS } diff --git a/src/Kook.Net.Core/Entities/Users/IFriendRequest.cs b/src/Kook.Net.Core/Entities/Users/IFriendRequest.cs index 78c97e8a..8b858b79 100644 --- a/src/Kook.Net.Core/Entities/Users/IFriendRequest.cs +++ b/src/Kook.Net.Core/Entities/Users/IFriendRequest.cs @@ -15,12 +15,12 @@ public interface IFriendRequest : IEntity /// /// The options to use when accepting this friend request. /// A task that represents the asynchronous accept operation. - Task AcceptAsync(RequestOptions options = null); + Task AcceptAsync(RequestOptions? options = null); /// /// Declines this friend request. /// /// The options to use when declining this friend request. /// A task that represents the asynchronous decline operation. - Task DeclineAsync(RequestOptions options = null); + Task DeclineAsync(RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Users/IGuildUser.cs b/src/Kook.Net.Core/Entities/Users/IGuildUser.cs index 8354df54..6ebb5995 100644 --- a/src/Kook.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Kook.Net.Core/Entities/Users/IGuildUser.cs @@ -13,7 +13,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A string representing the nickname of the user; null if none is set. /// - string Nickname { get; } + string? Nickname { get; } /// /// Gets the displayed name for this user. @@ -59,7 +59,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// true if the mobile number has been verified; false otherwise. /// - bool IsMobileVerified { get; } + bool? IsMobileVerified { get; } /// /// Gets when this user joined the guild. @@ -67,7 +67,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// The time of which the user has joined the guild. /// - DateTimeOffset JoinedAt { get; } + DateTimeOffset? JoinedAt { get; } /// /// Gets when this user was activated. @@ -75,7 +75,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// The time of which the user was activated. /// - DateTimeOffset ActiveAt { get; } + DateTimeOffset? ActiveAt { get; } /// /// Gets the color the user's displayed name is being displayed in. @@ -93,7 +93,7 @@ public interface IGuildUser : IUser, IVoiceState /// a gradient. /// /// - Color Color { get; } + Color? Color { get; } /// /// Gets whether this user owns the current guild. @@ -137,7 +137,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous kick operation. /// - Task KickAsync(RequestOptions options = null); + Task KickAsync(RequestOptions? options = null); /// /// Modifies this user's nickname in this guild. @@ -156,7 +156,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous modification operation. /// - Task ModifyNicknameAsync(string name, RequestOptions options = null); + Task ModifyNicknameAsync(string? name, RequestOptions? options = null); /// /// Gets all subscriptions of this user for this guild. @@ -164,9 +164,9 @@ public interface IGuildUser : IUser, IVoiceState /// The options to be used when sending the request. /// /// A task that represents the asynchronous retrieval operation. The task result contains - /// a collection of , each representing the subscriptions information. + /// a collection of , each representing the subscription information. /// - Task> GetBoostSubscriptionsAsync(RequestOptions options = null); + Task> GetBoostSubscriptionsAsync(RequestOptions? options = null); #endregion @@ -180,7 +180,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role addition operation. /// - Task AddRoleAsync(uint roleId, RequestOptions options = null); + Task AddRoleAsync(uint roleId, RequestOptions? options = null); /// /// Adds the specified role to this user in the guild. @@ -190,7 +190,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role addition operation. /// - Task AddRoleAsync(IRole role, RequestOptions options = null); + Task AddRoleAsync(IRole role, RequestOptions? options = null); /// /// Adds the specified to this user in the guild. @@ -200,7 +200,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role addition operation. /// - Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null); + Task AddRolesAsync(IEnumerable roleIds, RequestOptions? options = null); /// /// Adds the specified to this user in the guild. @@ -210,7 +210,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role addition operation. /// - Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); + Task AddRolesAsync(IEnumerable roles, RequestOptions? options = null); /// /// Removes the specified from this user in the guild. @@ -220,7 +220,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role removal operation. /// - Task RemoveRoleAsync(uint roleId, RequestOptions options = null); + Task RemoveRoleAsync(uint roleId, RequestOptions? options = null); /// /// Removes the specified from this user in the guild. @@ -230,7 +230,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role removal operation. /// - Task RemoveRoleAsync(IRole role, RequestOptions options = null); + Task RemoveRoleAsync(IRole role, RequestOptions? options = null); /// /// Removes the specified from this user in the guild. @@ -240,7 +240,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role removal operation. /// - Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null); + Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions? options = null); /// /// Removes the specified from this user in the guild. @@ -250,7 +250,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous role removal operation. /// - Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); + Task RemoveRolesAsync(IEnumerable roles, RequestOptions? options = null); #endregion @@ -263,7 +263,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous muting operation. /// - Task MuteAsync(RequestOptions options = null); + Task MuteAsync(RequestOptions? options = null); /// /// Deafen this user in this guild. @@ -272,7 +272,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous deafening operation. /// - Task DeafenAsync(RequestOptions options = null); + Task DeafenAsync(RequestOptions? options = null); /// /// Unmute this user in this guild. @@ -281,7 +281,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous unmuting operation. /// - Task UnmuteAsync(RequestOptions options = null); + Task UnmuteAsync(RequestOptions? options = null); /// /// Undeafen this user in this guild. @@ -290,7 +290,7 @@ public interface IGuildUser : IUser, IVoiceState /// /// A task that represents the asynchronous undeafening operation. /// - Task UndeafenAsync(RequestOptions options = null); + Task UndeafenAsync(RequestOptions? options = null); /// /// Gets a collection of voice channels a user. @@ -300,7 +300,7 @@ public interface IGuildUser : IUser, IVoiceState /// A task that represents the asynchronous get operation. The task result contains a collection of /// voice channels the user is connected to. /// - Task> GetConnectedVoiceChannelsAsync(RequestOptions options = null); + Task> GetConnectedVoiceChannelsAsync(RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Entities/Users/ISelfUser.cs b/src/Kook.Net.Core/Entities/Users/ISelfUser.cs index 01e49ecf..dd04d5c2 100644 --- a/src/Kook.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Kook.Net.Core/Entities/Users/ISelfUser.cs @@ -8,12 +8,12 @@ public interface ISelfUser : IUser /// /// Gets the mobile prefix of the logged-in user. /// - string MobilePrefix { get; } + string? MobilePrefix { get; } /// /// Gets the mobile number of the logged-in user. /// - string Mobile { get; } + string? Mobile { get; } /// /// TODO: To be documented. @@ -33,7 +33,7 @@ public interface ISelfUser : IUser /// /// A task that represents the asynchronous operation for starting a game activity. /// - Task StartPlayingAsync(IGame game, RequestOptions options = null); + Task StartPlayingAsync(IGame game, RequestOptions? options = null); /// /// Starts a new music activity. After this operation, a music activity will be displayed on the currently connected user's profile. @@ -43,7 +43,7 @@ public interface ISelfUser : IUser /// /// A task that represents the asynchronous operation for starting a music activity. /// - Task StartPlayingAsync(Music music, RequestOptions options = null); + Task StartPlayingAsync(Music music, RequestOptions? options = null); /// /// Stops an activity. After this operation, the activity on the currently connected user's profile will disappear. @@ -53,5 +53,5 @@ public interface ISelfUser : IUser /// /// A task that represents the asynchronous operation for stopping an activity. /// - Task StopPlayingAsync(ActivityType type, RequestOptions options = null); + Task StopPlayingAsync(ActivityType type, RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Users/IUser.cs b/src/Kook.Net.Core/Entities/Users/IUser.cs index 843cf7a9..eef63ce4 100644 --- a/src/Kook.Net.Core/Entities/Users/IUser.cs +++ b/src/Kook.Net.Core/Entities/Users/IUser.cs @@ -18,7 +18,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// /// Gets the per-username unique ID for this user. /// - ushort? IdentifyNumberValue { get; } + ushort IdentifyNumberValue { get; } /// /// Gets whether this user is a bot; null if unknown. @@ -53,7 +53,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// /// Gets the link to this user's banner. /// - string Banner { get; } + string? Banner { get; } /// /// Gets whether this user enabled denoise feature; null if unknown. @@ -63,7 +63,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// /// Get the tag this user has. /// - UserTag UserTag { get; } + UserTag? UserTag { get; } /// /// Gets the nameplates this user has. @@ -73,7 +73,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// /// Gets whether this user is a system user. /// - bool? IsSystemUser { get; } + bool IsSystemUser { get; } /// /// Creates the direct message channel of this user. @@ -93,7 +93,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// A task that represents the asynchronous operation for getting or creating a DM channel. The task result /// contains the DM channel associated with this user. /// - Task CreateDMChannelAsync(RequestOptions options = null); + Task CreateDMChannelAsync(RequestOptions? options = null); /// /// Gets the intimacy information with this user. @@ -103,7 +103,7 @@ public interface IUser : IEntity, IMentionable, IPresence /// A task that represents the asynchronous operation for getting the intimacy information. The task result /// contains the intimacy information associated with this user. /// - Task GetIntimacyAsync(RequestOptions options = null); + Task GetIntimacyAsync(RequestOptions? options = null); /// /// Updates the intimacy information with this user. @@ -111,33 +111,33 @@ public interface IUser : IEntity, IMentionable, IPresence /// A delegate containing the properties to modify the with. /// The options to be used when sending the request. /// A task that represents the asynchronous operation for updating the intimacy information. - Task UpdateIntimacyAsync(Action func, RequestOptions options = null); + Task UpdateIntimacyAsync(Action func, RequestOptions? options = null); /// /// Gets the friend state with this user. /// /// The options to be used when sending the request. /// A task that represents the asynchronous operation for getting the friend state. - Task BlockAsync(RequestOptions options = null); + Task BlockAsync(RequestOptions? options = null); /// /// Gets the friend state with this user. /// /// The options to be used when sending the request. /// A task that represents the asynchronous operation for getting the friend state. - Task UnblockAsync(RequestOptions options = null); + Task UnblockAsync(RequestOptions? options = null); /// /// Sends a friend request to this user. /// /// The options to be used when sending the request. /// A task that represents the asynchronous operation for sending the friend request. - Task RequestFriendAsync(RequestOptions options = null); + Task RequestFriendAsync(RequestOptions? options = null); /// /// Gets the friend state with this user. /// /// The options to be used when sending the request. /// A task that represents the asynchronous operation for getting the friend state. - Task RemoveFriendAsync(RequestOptions options = null); + Task RemoveFriendAsync(RequestOptions? options = null); } diff --git a/src/Kook.Net.Core/Entities/Users/IVoiceState.cs b/src/Kook.Net.Core/Entities/Users/IVoiceState.cs index fad23a9e..ab2ba875 100644 --- a/src/Kook.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Kook.Net.Core/Entities/Users/IVoiceState.cs @@ -30,5 +30,5 @@ public interface IVoiceState /// A generic voice channel object representing the voice channel that the user is currently in; null /// if none. /// - IVoiceChannel VoiceChannel { get; } + IVoiceChannel? VoiceChannel { get; } } diff --git a/src/Kook.Net.Core/Entities/Users/Nameplate.cs b/src/Kook.Net.Core/Entities/Users/Nameplate.cs index cfed93be..d3baa947 100644 --- a/src/Kook.Net.Core/Entities/Users/Nameplate.cs +++ b/src/Kook.Net.Core/Entities/Users/Nameplate.cs @@ -1,11 +1,12 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Representing a nameplate an can have. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class Nameplate : IEquatable { /// @@ -51,11 +52,12 @@ private Nameplate(string name, int type, string icon, string tips) #region IEquatable /// - public bool Equals(Nameplate other) + public bool Equals([NotNullWhen(true)] Nameplate? other) { - if (ReferenceEquals(null, other)) return false; - - if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; return Name == other.Name && Type == other.Type @@ -64,13 +66,14 @@ public bool Equals(Nameplate other) } /// - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { - if (ReferenceEquals(null, obj)) return false; - - if (ReferenceEquals(this, obj)) return true; - - if (obj.GetType() != GetType()) return false; + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; return Equals((Nameplate)obj); } diff --git a/src/Kook.Net.Core/Entities/Users/SearchGuildMemberProperties.cs b/src/Kook.Net.Core/Entities/Users/SearchGuildMemberProperties.cs index 17044a61..18bb7498 100644 --- a/src/Kook.Net.Core/Entities/Users/SearchGuildMemberProperties.cs +++ b/src/Kook.Net.Core/Entities/Users/SearchGuildMemberProperties.cs @@ -10,7 +10,7 @@ public class SearchGuildMemberProperties /// Gets or sets the name of the user to be searched for; /// null to not search via a name. /// - public string SearchName { get; set; } + public string? SearchName { get; set; } /// /// Gets or sets the ID of the role the user must have to be searched for; diff --git a/src/Kook.Net.Core/Entities/Users/UserTag.cs b/src/Kook.Net.Core/Entities/Users/UserTag.cs index 988d9eae..c30c2466 100644 --- a/src/Kook.Net.Core/Entities/Users/UserTag.cs +++ b/src/Kook.Net.Core/Entities/Users/UserTag.cs @@ -1,11 +1,12 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; /// /// Representing a tag an can have. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class UserTag : IEquatable { /// @@ -61,23 +62,24 @@ private UserTag(Color color, AlphaColor backgroundColor, string text) #region IEquatable /// - public bool Equals(UserTag other) + public bool Equals([NotNullWhen(true)] UserTag? other) { - if (ReferenceEquals(null, other)) return false; - - if (ReferenceEquals(this, other)) return true; - + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; return Text == other.Text; } /// - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { - if (ReferenceEquals(null, obj)) return false; - - if (ReferenceEquals(this, obj)) return true; - - if (obj.GetType() != GetType()) return false; + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; return Equals((UserTag)obj); } diff --git a/src/Kook.Net.Core/Extensions/CardExtensions.cs b/src/Kook.Net.Core/Extensions/CardExtensions.cs index 18b00c33..7e491903 100644 --- a/src/Kook.Net.Core/Extensions/CardExtensions.cs +++ b/src/Kook.Net.Core/Extensions/CardExtensions.cs @@ -10,17 +10,16 @@ public static class CardExtensions /// /// Converts the to a with the same properties. /// - public static IElementBuilder ToBuilder(this IElement element) + public static IElementBuilder ToBuilder(this IElement entity) { - if (element is null) return null; - return element.Type switch + return entity switch { - ElementType.PlainText => (element as PlainTextElement).ToBuilder(), - ElementType.KMarkdown => (element as KMarkdownElement).ToBuilder(), - ElementType.Image => (element as ImageElement).ToBuilder(), - ElementType.Button => (element as ButtonElement).ToBuilder(), - ElementType.Paragraph => (element as ParagraphStruct).ToBuilder(), - _ => throw new ArgumentOutOfRangeException(nameof(element)) + PlainTextElement { Type: ElementType.PlainText } plainTextElement => plainTextElement.ToBuilder(), + KMarkdownElement { Type: ElementType.KMarkdown } kMarkdownElement => kMarkdownElement.ToBuilder(), + ImageElement { Type: ElementType.Image } imageElement => imageElement.ToBuilder(), + ButtonElement { Type: ElementType.Button } buttonElement => buttonElement.ToBuilder(), + ParagraphStruct { Type: ElementType.Paragraph } paragraphStruct => paragraphStruct.ToBuilder(), + _ => throw new ArgumentOutOfRangeException(nameof(entity)) }; } @@ -29,8 +28,11 @@ public static IElementBuilder ToBuilder(this IElement element) /// public static PlainTextElementBuilder ToBuilder(this PlainTextElement entity) { - if (entity is null) return null; - return new PlainTextElementBuilder { Content = entity.Content, Emoji = entity.Emoji }; + return new PlainTextElementBuilder + { + Content = entity.Content, + Emoji = entity.Emoji ?? true + }; } /// @@ -38,19 +40,20 @@ public static PlainTextElementBuilder ToBuilder(this PlainTextElement entity) /// public static KMarkdownElementBuilder ToBuilder(this KMarkdownElement entity) { - if (entity is null) return null; return new KMarkdownElementBuilder { Content = entity.Content }; } /// - /// Converts the to a with the same properties. + /// Converts the to an with the same properties. /// public static ImageElementBuilder ToBuilder(this ImageElement entity) { - if (entity is null) return null; return new ImageElementBuilder { - Source = entity.Source, Alternative = entity.Alternative, Size = entity.Size, Circle = entity.Circle + Source = entity.Source, + Alternative = entity.Alternative, + Size = entity.Size, + Circle = entity.Circle }; } @@ -59,10 +62,12 @@ public static ImageElementBuilder ToBuilder(this ImageElement entity) /// public static ButtonElementBuilder ToBuilder(this ButtonElement entity) { - if (entity is null) return null; return new ButtonElementBuilder { - Theme = entity.Theme, Click = entity.Click, Value = entity.Value, Text = entity.Text.ToBuilder() + Theme = entity.Theme ?? ButtonTheme.Primary, + Click = entity.Click ?? ButtonClickEventType.None, + Value = entity.Value, + Text = entity.Text.ToBuilder() }; } @@ -71,10 +76,10 @@ public static ButtonElementBuilder ToBuilder(this ButtonElement entity) /// public static ParagraphStructBuilder ToBuilder(this ParagraphStruct entity) { - if (entity is null) return null; return new ParagraphStructBuilder { - ColumnCount = entity.ColumnCount, Fields = entity.Fields.Select(x => x.ToBuilder()).ToList() + ColumnCount = entity.ColumnCount ?? 1, + Fields = entity.Fields.Select(x => x.ToBuilder()).ToList() }; } @@ -87,21 +92,20 @@ public static ParagraphStructBuilder ToBuilder(this ParagraphStruct entity) /// public static IModuleBuilder ToBuilder(this IModule entity) { - if (entity is null) return null; - return entity.Type switch + return entity switch { - ModuleType.Header => (entity as HeaderModule).ToBuilder(), - ModuleType.Section => (entity as SectionModule).ToBuilder(), - ModuleType.ImageGroup => (entity as ImageGroupModule).ToBuilder(), - ModuleType.Container => (entity as ContainerModule).ToBuilder(), - ModuleType.ActionGroup => (entity as ActionGroupModule).ToBuilder(), - ModuleType.Context => (entity as ContextModule).ToBuilder(), - ModuleType.Divider => (entity as DividerModule).ToBuilder(), - ModuleType.File => (entity as FileModule).ToBuilder(), - ModuleType.Audio => (entity as AudioModule).ToBuilder(), - ModuleType.Video => (entity as VideoModule).ToBuilder(), - ModuleType.Countdown => (entity as CountdownModule).ToBuilder(), - ModuleType.Invite => (entity as InviteModule).ToBuilder(), + HeaderModule { Type: ModuleType.Header } headerModule => headerModule.ToBuilder(), + SectionModule { Type: ModuleType.Section } sectionModule => sectionModule.ToBuilder(), + ImageGroupModule { Type: ModuleType.ImageGroup } imageGroupModule => imageGroupModule.ToBuilder(), + ContainerModule { Type: ModuleType.Container } containerModule => containerModule.ToBuilder(), + ActionGroupModule { Type: ModuleType.ActionGroup } actionGroupModule => actionGroupModule.ToBuilder(), + ContextModule { Type: ModuleType.Context } contextModule => contextModule.ToBuilder(), + DividerModule { Type: ModuleType.Divider } dividerModule => dividerModule.ToBuilder(), + FileModule { Type: ModuleType.File } fileModule => fileModule.ToBuilder(), + AudioModule { Type: ModuleType.Audio } audioModule => audioModule.ToBuilder(), + VideoModule { Type: ModuleType.Video } videoModule => videoModule.ToBuilder(), + CountdownModule { Type: ModuleType.Countdown } countdownModule => countdownModule.ToBuilder(), + InviteModule { Type: ModuleType.Invite } inviteModule => inviteModule.ToBuilder(), _ => throw new ArgumentOutOfRangeException(nameof(entity)) }; } @@ -111,8 +115,10 @@ public static IModuleBuilder ToBuilder(this IModule entity) /// public static HeaderModuleBuilder ToBuilder(this HeaderModule entity) { - if (entity is null) return null; - return new HeaderModuleBuilder { Text = entity.Text.ToBuilder() }; + return new HeaderModuleBuilder + { + Text = entity.Text?.ToBuilder() + }; } /// @@ -120,12 +126,11 @@ public static HeaderModuleBuilder ToBuilder(this HeaderModule entity) /// public static SectionModuleBuilder ToBuilder(this SectionModule entity) { - if (entity is null) return null; return new SectionModuleBuilder { Mode = entity.Mode, - Text = entity.Text.ToBuilder(), - Accessory = entity.Accessory.ToBuilder() + Text = entity.Text?.ToBuilder(), + Accessory = entity.Accessory?.ToBuilder() }; } @@ -134,8 +139,10 @@ public static SectionModuleBuilder ToBuilder(this SectionModule entity) /// public static ImageGroupModuleBuilder ToBuilder(this ImageGroupModule entity) { - if (entity is null) return null; - return new ImageGroupModuleBuilder { Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() }; + return new ImageGroupModuleBuilder + { + Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() + }; } /// @@ -143,8 +150,10 @@ public static ImageGroupModuleBuilder ToBuilder(this ImageGroupModule entity) /// public static ContainerModuleBuilder ToBuilder(this ContainerModule entity) { - if (entity is null) return null; - return new ContainerModuleBuilder { Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() }; + return new ContainerModuleBuilder + { + Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() + }; } /// @@ -152,8 +161,10 @@ public static ContainerModuleBuilder ToBuilder(this ContainerModule entity) /// public static ActionGroupModuleBuilder ToBuilder(this ActionGroupModule entity) { - if (entity is null) return null; - return new ActionGroupModuleBuilder { Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() }; + return new ActionGroupModuleBuilder + { + Elements = entity.Elements.Select(x => x.ToBuilder()).ToList() + }; } /// @@ -161,16 +172,17 @@ public static ActionGroupModuleBuilder ToBuilder(this ActionGroupModule entity) /// public static ContextModuleBuilder ToBuilder(this ContextModule entity) { - if (entity is null) return null; - return new ContextModuleBuilder { Elements = entity.Elements.Select(e => e.ToBuilder()).ToList() }; + return new ContextModuleBuilder + { + Elements = entity.Elements.Select(e => e.ToBuilder()).ToList() + }; } /// /// Converts the to a with the same properties. /// - public static DividerModuleBuilder ToBuilder(this DividerModule entity) + public static DividerModuleBuilder ToBuilder(this DividerModule _) { - if (entity is null) return null; return new DividerModuleBuilder(); } @@ -179,17 +191,24 @@ public static DividerModuleBuilder ToBuilder(this DividerModule entity) /// public static FileModuleBuilder ToBuilder(this FileModule entity) { - if (entity is null) return null; - return new FileModuleBuilder { Source = entity.Source, Title = entity.Title }; + return new FileModuleBuilder + { + Source = entity.Source, + Title = entity.Title + }; } /// - /// Converts the to a with the same properties. + /// Converts the to an with the same properties. /// public static AudioModuleBuilder ToBuilder(this AudioModule entity) { - if (entity is null) return null; - return new AudioModuleBuilder { Source = entity.Source, Title = entity.Title, Cover = entity.Cover }; + return new AudioModuleBuilder + { + Source = entity.Source, + Title = entity.Title, + Cover = entity.Cover + }; } /// @@ -197,8 +216,11 @@ public static AudioModuleBuilder ToBuilder(this AudioModule entity) /// public static VideoModuleBuilder ToBuilder(this VideoModule entity) { - if (entity is null) return null; - return new VideoModuleBuilder { Source = entity.Source, Title = entity.Title }; + return new VideoModuleBuilder + { + Source = entity.Source, + Title = entity.Title + }; } /// @@ -206,20 +228,23 @@ public static VideoModuleBuilder ToBuilder(this VideoModule entity) /// public static CountdownModuleBuilder ToBuilder(this CountdownModule entity) { - if (entity is null) return null; return new CountdownModuleBuilder { - Mode = entity.Mode, EndTime = entity.EndTime, StartTime = entity.StartTime + Mode = entity.Mode, + EndTime = entity.EndTime, + StartTime = entity.StartTime }; } /// - /// Converts the to a with the same properties. + /// Converts the to an with the same properties. /// public static InviteModuleBuilder ToBuilder(this InviteModule entity) { - if (entity is null) return null; - return new InviteModuleBuilder { Code = entity.Code }; + return new InviteModuleBuilder + { + Code = entity.Code + }; } #endregion @@ -227,32 +252,28 @@ public static InviteModuleBuilder ToBuilder(this InviteModule entity) #region Cards /// - /// Converts the to a with the same properties. + /// Converts the to an with the same properties. /// - public static ICardBuilder ToBuilder(this ICard builder) + public static ICardBuilder ToBuilder(this ICard entity) { - if (builder is null) return null; - - return builder.Type switch + return entity switch { - CardType.Card => (builder as Card).ToBuilder(), - _ => throw new ArgumentOutOfRangeException(nameof(builder)) + Card { Type: CardType.Card } card => card.ToBuilder(), + _ => throw new ArgumentOutOfRangeException(nameof(entity)) }; } /// /// Converts the to a with the same properties. /// - public static CardBuilder ToBuilder(this Card builder) + public static CardBuilder ToBuilder(this Card entity) { - if (builder is null) return null; - return new CardBuilder { - Theme = builder.Theme, - Size = builder.Size, - Color = builder.Color, - Modules = builder.Modules.Select(m => m.ToBuilder()).ToList() + Theme = entity.Theme, + Size = entity.Size, + Color = entity.Color, + Modules = entity.Modules.Select(m => m.ToBuilder()).ToList() }; } diff --git a/src/Kook.Net.Core/Extensions/CollectionExtensions.cs b/src/Kook.Net.Core/Extensions/CollectionExtensions.cs index e05ead26..636c41a6 100644 --- a/src/Kook.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Kook.Net.Core/Extensions/CollectionExtensions.cs @@ -7,24 +7,44 @@ internal static class CollectionExtensions { //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyCollection source) // => new CollectionWrapper(source, () => source.Count); - public static IReadOnlyCollection ToReadOnlyCollection(this ICollection source) - => new CollectionWrapper(source, () => source.Count); + + public static IReadOnlyCollection ToReadOnlyCollection(this ICollection source) => + new CollectionWrapper(source, () => source.Count); //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); - public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) - => new CollectionWrapper(source.Values, () => source.Count); + + public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) => + new CollectionWrapper(source.Values, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, - IReadOnlyCollection source) - => new CollectionWrapper(query, () => source.Count); + IReadOnlyCollection source) => + new CollectionWrapper(query, () => source.Count); + + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) => + new CollectionWrapper(query, countFunc); + +#if !NET6_0_OR_GREATER + public static TSource? MinBy( + this IEnumerable source, + Func keySelector) => + source.OrderBy(keySelector).FirstOrDefault(); + + public static TSource? MaxBy( + this IEnumerable source, + Func keySelector) => + source.OrderByDescending(keySelector).FirstOrDefault(); - public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) - => new CollectionWrapper(query, countFunc); + public static IEnumerable> Chunk(this IEnumerable source, int chunkSize) => + source + .Select((x, i) => new { Index = i, Value = x }) + .GroupBy(x => x.Index / chunkSize) + .Select(x => x.Select(v => v.Value)); +#endif } -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -internal struct CollectionWrapper : IReadOnlyCollection +[DebuggerDisplay("{DebuggerDisplay,nq}")] +internal readonly struct CollectionWrapper : IReadOnlyCollection { private readonly IEnumerable _query; private readonly Func _countFunc; diff --git a/src/Kook.Net.Core/Extensions/MessageExtensions.cs b/src/Kook.Net.Core/Extensions/MessageExtensions.cs index dff8088a..1259455d 100644 --- a/src/Kook.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Kook.Net.Core/Extensions/MessageExtensions.cs @@ -31,9 +31,9 @@ public static string GetJumpUrl(this IMessage msg) /// /// /// - /// IEmote A = new Emoji("🅰"); - /// IEmote B = new Emoji("🅱"); - /// await msg.AddReactionsAsync(new[] { A, B }); + /// IEmote a = new Emoji("🅰"); + /// IEmote b = new Emoji("🅱"); + /// await msg.AddReactionsAsync([a, b]); /// /// /// The message to add reactions to. @@ -44,9 +44,11 @@ public static string GetJumpUrl(this IMessage msg) /// /// /// - public static async Task AddReactionsAsync(this IUserMessage msg, IEnumerable reactions, RequestOptions options = null) + public static async Task AddReactionsAsync(this IUserMessage msg, IEnumerable reactions, + RequestOptions? options = null) { - foreach (IEmote rxn in reactions) await msg.AddReactionAsync(rxn, options).ConfigureAwait(false); + foreach (IEmote emote in reactions) + await msg.AddReactionAsync(emote, options).ConfigureAwait(false); } /// @@ -57,7 +59,7 @@ public static async Task AddReactionsAsync(this IUserMessage msg, IEnumerable /// /// - /// await msg.RemoveReactionsAsync(currentUser, new[] { A, B }); + /// await msg.RemoveReactionsAsync(currentUser, [A, B]); /// /// /// The message to remove reactions from. @@ -69,9 +71,11 @@ public static async Task AddReactionsAsync(this IUserMessage msg, IEnumerable /// /// - public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEnumerable reactions, RequestOptions options = null) + public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEnumerable reactions, + RequestOptions? options = null) { - foreach (IEmote rxn in reactions) await msg.RemoveReactionAsync(rxn, user, options).ConfigureAwait(false); + foreach (IEmote emote in reactions) + await msg.RemoveReactionAsync(emote, user, options).ConfigureAwait(false); } /// @@ -79,32 +83,38 @@ public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, /// /// The message that is being replied on. /// The file path of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// true if the source message will be quoted in this message; otherwise, false. /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The options to be used when sending the request. public static async Task> ReplyFileAsync(this IUserMessage message, - string path, string fileName = null, AttachmentType type = AttachmentType.File, bool isQuote = false, bool isEphemeral = false, - RequestOptions options = null) => - await message.Channel.SendFileAsync(path, fileName, type, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + string path, string? filename = null, AttachmentType type = AttachmentType.File, bool isQuote = false, + bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendFileAsync(path, filename, type, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); /// /// Sends an inline reply of file that references a message. /// /// The message that is being replied on. /// Stream of the file to be sent. - /// The name of the file. + /// The name of the file. /// The type of the file. /// true if the source message will be quoted in this message; otherwise, false. /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The options to be used when sending the request. public static async Task> ReplyFileAsync(this IUserMessage message, - Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, bool isQuote = false, bool isEphemeral = false, - RequestOptions options = null) => - await message.Channel.SendFileAsync(stream, fileName, type, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + Stream stream, string filename, AttachmentType type = AttachmentType.File, bool isQuote = false, + bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendFileAsync(stream, filename, type, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); /// /// Sends an inline reply of file that references a message. @@ -115,9 +125,12 @@ public static async Task> ReplyFileAsync(this IUse /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The options to be used when sending the request. public static async Task> ReplyFileAsync(this IUserMessage message, - FileAttachment attachment, bool isQuote = false, bool isEphemeral = false, RequestOptions options = null) => - await message.Channel.SendFileAsync(attachment, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + FileAttachment attachment, bool isQuote = false, bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendFileAsync(attachment, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); /// /// Sends an inline reply of text that references a message. @@ -128,9 +141,12 @@ public static async Task> ReplyFileAsync(this IUse /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The request options for this async request. public static async Task> ReplyTextAsync(this IUserMessage message, - string content, bool isQuote = false, bool isEphemeral = false, RequestOptions options = null) => - await message.Channel.SendTextAsync(content, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + string content, bool isQuote = false, bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendTextAsync(content, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); /// /// Sends a card message to the source channel. @@ -141,9 +157,12 @@ public static async Task> ReplyTextAsync(this IUse /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The request options for this async request. public static async Task> ReplyCardsAsync(this IUserMessage message, - IEnumerable cards, bool isQuote = false, bool isEphemeral = false, RequestOptions options = null) => - await message.Channel.SendCardsAsync(cards, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + IEnumerable cards, bool isQuote = false, bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendCardsAsync(cards, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); /// /// Sends a card message to the source channel. @@ -153,9 +172,11 @@ public static async Task> ReplyCardsAsync(this IUs /// true if the source message will be quoted in this message; otherwise, false. /// true if the message to be sent can be seen only by the command invoker; otherwise, false. /// The request options for this async request. - public static async Task> ReplyCardAsync( - this IUserMessage message, - ICard card, bool isQuote = false, bool isEphemeral = false, RequestOptions options = null) => - await message.Channel.SendCardAsync(card, isQuote ? new Quote(message.Id) : null, - isEphemeral ? message.Author : null, options).ConfigureAwait(false); + public static async Task> ReplyCardAsync(this IUserMessage message, + ICard card, bool isQuote = false, bool isEphemeral = false, RequestOptions? options = null) => + await message.Channel.SendCardAsync(card, + isQuote ? new MessageReference(message.Id) : null, + isEphemeral ? message.Author : null, + options) + .ConfigureAwait(false); } diff --git a/src/Kook.Net.Core/Extensions/TaskCompletionSourceExtensions.cs b/src/Kook.Net.Core/Extensions/TaskCompletionSourceExtensions.cs index dd7e397f..7635f593 100644 --- a/src/Kook.Net.Core/Extensions/TaskCompletionSourceExtensions.cs +++ b/src/Kook.Net.Core/Extensions/TaskCompletionSourceExtensions.cs @@ -2,21 +2,21 @@ namespace Kook; internal static class TaskCompletionSourceExtensions { - public static Task SetResultAsync(this TaskCompletionSource source, T result) - => Task.Run(() => source.SetResult(result)); + public static Task SetResultAsync(this TaskCompletionSource source, T result) => + Task.Run(() => source.SetResult(result)); - public static Task TrySetResultAsync(this TaskCompletionSource source, T result) - => Task.Run(() => source.TrySetResult(result)); + public static Task TrySetResultAsync(this TaskCompletionSource source, T result) => + Task.Run(() => source.TrySetResult(result)); - public static Task SetExceptionAsync(this TaskCompletionSource source, Exception ex) - => Task.Run(() => source.SetException(ex)); + public static Task SetExceptionAsync(this TaskCompletionSource source, Exception ex) => + Task.Run(() => source.SetException(ex)); - public static Task TrySetExceptionAsync(this TaskCompletionSource source, Exception ex) - => Task.Run(() => source.TrySetException(ex)); + public static Task TrySetExceptionAsync(this TaskCompletionSource source, Exception ex) => + Task.Run(() => source.TrySetException(ex)); - public static Task SetCanceledAsync(this TaskCompletionSource source) - => Task.Run(() => source.SetCanceled()); + public static Task SetCanceledAsync(this TaskCompletionSource source) => + Task.Run(source.SetCanceled); - public static Task TrySetCanceledAsync(this TaskCompletionSource source) - => Task.Run(() => source.TrySetCanceled()); + public static Task TrySetCanceledAsync(this TaskCompletionSource source) => + Task.Run(source.TrySetCanceled); } diff --git a/src/Kook.Net.Core/Extensions/UserExtensions.cs b/src/Kook.Net.Core/Extensions/UserExtensions.cs index b0bf1cc4..a4d2bed6 100644 --- a/src/Kook.Net.Core/Extensions/UserExtensions.cs +++ b/src/Kook.Net.Core/Extensions/UserExtensions.cs @@ -8,29 +8,34 @@ public static class UserExtensions /// /// The user to send the DM to. /// The file path of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. public static async Task> SendFileAsync(this IUser user, - string path, string fileName = null, AttachmentType type = AttachmentType.File, IQuote quote = null, - RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(path, fileName, type, quote, options).ConfigureAwait(false); + string path, string? filename = null, AttachmentType type = AttachmentType.File, IQuote? quote = null, + RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendFileAsync(path, filename, type, quote, options).ConfigureAwait(false); + } /// /// Sends a file via DM. /// /// The user to send the DM to. /// The stream of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. public static async Task> SendFileAsync(this IUser user, - Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, IQuote quote = null, - RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)) - .SendFileAsync(stream, fileName, type, quote, options).ConfigureAwait(false); + Stream stream, string filename, AttachmentType type = AttachmentType.File, IQuote? quote = null, + RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendFileAsync(stream, filename, type, quote, options).ConfigureAwait(false); + } /// /// Sends a file via DM. @@ -40,8 +45,11 @@ public static async Task> SendFileAsync(this IUser /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. public static async Task> SendFileAsync(this IUser user, - FileAttachment attachment, IQuote quote = null, RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(attachment, quote, options).ConfigureAwait(false); + FileAttachment attachment, IQuote? quote = null, RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendFileAsync(attachment, quote, options).ConfigureAwait(false); + } /// /// Sends a text message via DM. @@ -51,10 +59,11 @@ public static async Task> SendFileAsync(this IUser /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. public static async Task> SendTextAsync(this IUser user, - string content, - IQuote quote = null, - RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendTextAsync(content, quote, options).ConfigureAwait(false); + string content, IQuote? quote = null, RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendTextAsync(content, quote, options).ConfigureAwait(false); + } /// /// Sends a card message message via DM. @@ -64,8 +73,11 @@ public static async Task> SendTextAsync(this IUser /// The message quote to be included. Used to reply to specific messages. /// The request options for this async request. public static async Task> SendCardsAsync(this IUser user, - IEnumerable cards, IQuote quote = null, RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendCardsAsync(cards, quote, options).ConfigureAwait(false); + IEnumerable cards, IQuote? quote = null, RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendCardsAsync(cards, quote, options).ConfigureAwait(false); + } /// /// Sends a card message message via DM. @@ -75,8 +87,11 @@ public static async Task> SendCardsAsync(this IUse /// The message quote to be included. Used to reply to specific messages. /// The request options for this async request. public static async Task> SendCardAsync(this IUser user, - ICard card, IQuote quote = null, RequestOptions options = null) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendCardAsync(card, quote, options).ConfigureAwait(false); + ICard card, IQuote? quote = null, RequestOptions? options = null) + { + IDMChannel dmChannel = await user.CreateDMChannelAsync().ConfigureAwait(false); + return await dmChannel.SendCardAsync(card, quote, options).ConfigureAwait(false); + } /// /// Bans the user from the guild and optionally prunes their recent messages. @@ -89,6 +104,7 @@ public static async Task> SendCardAsync(this IUser /// /// A task that represents the asynchronous operation for banning a user. /// - public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) - => user.Guild.AddBanAsync(user, pruneDays, reason, options); + public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string? reason = null, + RequestOptions? options = null) => + user.Guild.AddBanAsync(user, pruneDays, reason, options); } diff --git a/src/Kook.Net.Core/Format.cs b/src/Kook.Net.Core/Format.cs index 73e41af5..e072421c 100644 --- a/src/Kook.Net.Core/Format.cs +++ b/src/Kook.Net.Core/Format.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.RegularExpressions; @@ -8,7 +9,7 @@ namespace Kook; /// public static class Format { - private static readonly string[] SensitiveCharacters = { "\\", "*", "~", "`", ":", "-", "]", ")", ">" }; + private static readonly string[] SensitiveCharacters = ["\\", "*", "~", "`", ":", "-", "]", ")", ">"]; /// Returns a markdown-formatted string with bold formatting. /// The text to format. @@ -18,7 +19,7 @@ public static class Format /// Set to true will sanitize the text by replacing all occurrences of /// * with \*. /// - public static string Bold(this string text, bool sanitize = true) => + public static string Bold(this string? text, bool sanitize = true) => $"**{(sanitize ? text.Sanitize("*") : text)}**"; /// Returns a markdown-formatted string with italics formatting. @@ -29,7 +30,7 @@ public static string Bold(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// * with \*. /// - public static string Italics(this string text, bool sanitize = true) => + public static string Italics(this string? text, bool sanitize = true) => $"*{(sanitize ? text.Sanitize("*") : text)}*"; /// Returns a markdown-formatted string with bold italics formatting. @@ -40,7 +41,7 @@ public static string Italics(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// * with \*. /// - public static string BoldItalics(this string text, bool sanitize = true) => + public static string BoldItalics(this string? text, bool sanitize = true) => $"***{(sanitize ? text.Sanitize("*") : text)}***"; /// Returns a markdown-formatted string with strike-through formatting. @@ -51,7 +52,7 @@ public static string BoldItalics(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// ~ with \~. /// - public static string Strikethrough(this string text, bool sanitize = true) => + public static string Strikethrough(this string? text, bool sanitize = true) => $"~~{(sanitize ? text.Sanitize("~") : text)}~~"; /// Returns a markdown-formatted string colored with the specified . @@ -68,7 +69,7 @@ public static string Strikethrough(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// ( and ) with \( and \). /// - public static string Colorize(this string text, TextTheme theme, bool sanitize = true) => + public static string Colorize(this string? text, TextTheme theme, bool sanitize = true) => $"(font){(sanitize ? text.Sanitize("(", ")") : text)}(font)[{theme.ToString().ToLowerInvariant()}]"; /// Returns a markdown-formatted URL. @@ -81,12 +82,13 @@ public static string Colorize(this string text, TextTheme theme, bool sanitize = /// [ and ] with \[ and \], and the URL by replacing all occurrences of /// ( and ) with \( and \). /// - public static string Url(this string text, string url, bool sanitize = true) => + public static string Url(this string? text, string url, bool sanitize = true) => $"[{(sanitize ? text.Sanitize("[", "]") : text)}]({(sanitize ? url.Sanitize("(", ")") : url)})"; /// - public static string Url(this string text, Uri url, bool sanitize = true) => text.Url(url.ToString(), sanitize); + public static string Url(this string? text, Uri url, bool sanitize = true) => + text.Url(url.ToString(), sanitize); /// Sanitizes the string, safely escaping any Markdown sequences. /// The text to sanitize. @@ -96,14 +98,12 @@ public static string Url(this string text, string url, bool sanitize = true) => /// If no sensitive characters are specified, the default sensitive characters are used. /// The default sensitive characters are: \, *, ~, `, :, -, ], ), >. /// - public static string Sanitize(this string text, params string[] sensitiveCharacters) + [return: NotNullIfNotNull(nameof(text))] + public static string? Sanitize(this string? text, params string[] sensitiveCharacters) { if (text is null) return null; - - if (sensitiveCharacters is not { Length: > 0 }) - return SensitiveCharacters.Aggregate(text, - (current, unsafeChar) => current.Replace(unsafeChar, $"\\{unsafeChar}")); - return sensitiveCharacters.Aggregate(text, + string[] sensitiveChars = sensitiveCharacters.Length > 0 ? sensitiveCharacters : SensitiveCharacters; + return sensitiveChars.Aggregate(text, (current, unsafeChar) => current.Replace(unsafeChar, $"\\{unsafeChar}")); } @@ -133,17 +133,19 @@ public static string Sanitize(this string text, params string[] sensitiveCharact /// /// /// - public static string Quote(this string text, bool sanitize = true) + [return: NotNullIfNotNull(nameof(text))] + public static string? Quote(this string? text, bool sanitize = true) { - string escaped = sanitize ? text.Sanitize(">") : text; + string? escaped = sanitize ? text.Sanitize(">") : text; // do not modify null or whitespace text // whitespace does not get quoted properly - if (string.IsNullOrWhiteSpace(escaped)) return escaped; + if (escaped is null || string.IsNullOrWhiteSpace(escaped)) + return escaped; StringBuilder result = new(); - string lineEnding = null; + string? lineEnding = null; int textLength = escaped.Length; for (int i = 0; i < textLength; i++) { @@ -241,7 +243,7 @@ public static string Quote(this string text, bool sanitize = true) /// Set to true will sanitize the text by replacing all occurrences of /// ( and ) with \( and \). /// - public static string Underline(this string text, bool sanitize = true) => + public static string Underline(this string? text, bool sanitize = true) => $"(ins){(sanitize ? text.Sanitize("(", ")") : text)}(ins)"; /// Returns a string with spoiler formatting. @@ -252,7 +254,7 @@ public static string Underline(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// ( and ) with \( and \). /// - public static string Spoiler(this string text, bool sanitize = true) => + public static string Spoiler(this string? text, bool sanitize = true) => $"(spl){(sanitize ? text.Sanitize("(", ")") : text)}(spl)"; /// Returns a markdown-formatted string with inline code or code block formatting. @@ -264,9 +266,11 @@ public static string Spoiler(this string text, bool sanitize = true) => /// Set to true will sanitize the text by replacing all occurrences of /// ` with \`. /// - public static string Code(this string text, string language = null, bool sanitize = true) + public static string Code(this string? text, string? language = null, bool sanitize = true) { - string newLine = null; + if (text is null) return "`\u200d`"; + + string? newLine = null; int length = text.Length; for (int i = 0; i < length; i++) if (text[i] is '\r' or '\n') @@ -292,9 +296,10 @@ public static string Code(this string text, string language = null, bool sanitiz /// Set to true will sanitize the text by replacing all occurrences of /// ` with \`. /// - public static string CodeBlock(this string text, string language = null, bool sanitize = true) + public static string CodeBlock(this string? text, string? language = null, bool sanitize = true) { - string newLine = null; + if (text is null) return "```\n\n```"; + string? newLine = null; int length = text.Length; for (int i = 0; i < length; i++) if (text[i] is '\r' or '\n') @@ -333,8 +338,10 @@ public static string CodeBlock(this string text, string language = null, bool sa /// /// /// - public static string BlockQuote(this string text, bool sanitize = true) + public static string BlockQuote(this string? text, bool sanitize = true) { + if (text is null) return "> \u200d"; + string escaped = sanitize ? text.Sanitize(">") : text; // do not modify null or whitespace text @@ -362,13 +369,9 @@ public static string BlockQuote(this string text, bool sanitize = true) /// /// The to remove markdown from. /// Gets the unformatted text. - public static string StripMarkDown(this string text) - { + public static string StripMarkDown(this string text) => // Remove KOOK supported markdown - string newText = Regex.Replace(text, @"(\*|\(ins\)|\(spl\)|`|~|>|\\)", ""); - - return newText; - } + Regex.Replace(text, @"(\*|\(ins\)|\(spl\)|`|~|>|\\)", "", RegexOptions.Compiled); /// /// Formats a user's username + identify number while maintaining bidirectional unicode diff --git a/src/Kook.Net.Core/IKookClient.cs b/src/Kook.Net.Core/IKookClient.cs index 63e9f589..4c8d6280 100644 --- a/src/Kook.Net.Core/IKookClient.cs +++ b/src/Kook.Net.Core/IKookClient.cs @@ -15,7 +15,7 @@ public interface IKookClient : IDisposable /// /// Gets the currently logged-in user. /// - ISelfUser CurrentUser { get; } + ISelfUser? CurrentUser { get; } /// /// Gets the token type of the logged-in user. @@ -59,7 +59,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains the channel associated /// with the identifier; null when the channel cannot be found. /// - Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a direct message channel. @@ -71,7 +71,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of direct-message channels that the user currently partakes in. /// - Task GetDMChannelAsync(Guid chatCode, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetDMChannelAsync(Guid chatCode, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a collection of direct message channels opened in this session. @@ -89,7 +89,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of direct-message channels that the user currently partakes in. /// - Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -105,7 +105,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains the guild associated /// with the identifier; null when the guild cannot be found. /// - Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a collection of guilds that the user is currently in. @@ -116,7 +116,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of guilds that the current user is in. /// - Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion @@ -132,7 +132,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains the user associated with /// the identifier; null if the user is not found. /// - Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets a user. @@ -144,7 +144,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains the user associated with /// the name and the identifyNumber; null if the user is not found. /// - Task GetUserAsync(string username, string identifyNumber, RequestOptions options = null); + Task GetUserAsync(string username, string identifyNumber, RequestOptions? options = null); #endregion @@ -159,7 +159,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a collection of users /// that are friends with the current user. /// - Task> GetFriendsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetFriendsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets friend requests. @@ -170,7 +170,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a collection of users /// that requested to be friends with the current user. /// - Task> GetFriendRequestsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetFriendRequestsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); /// /// Gets blocked users. @@ -181,7 +181,7 @@ public interface IKookClient : IDisposable /// A task that represents the asynchronous get operation. The task result contains a collection of users /// that are blocked by the current user. /// - Task> GetBlockedUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetBlockedUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null); #endregion } diff --git a/src/Kook.Net.Core/Kook.Net.Core.csproj b/src/Kook.Net.Core/Kook.Net.Core.csproj index 88f773c1..a68731bf 100644 --- a/src/Kook.Net.Core/Kook.Net.Core.csproj +++ b/src/Kook.Net.Core/Kook.Net.Core.csproj @@ -12,7 +12,14 @@ - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Kook.Net.Core/KookConfig.cs b/src/Kook.Net.Core/KookConfig.cs index 227f87b5..92afbc91 100644 --- a/src/Kook.Net.Core/KookConfig.cs +++ b/src/Kook.Net.Core/KookConfig.cs @@ -22,7 +22,7 @@ public class KookConfig /// Returns the Voice API version Kook.Net uses. /// /// - /// An representing the API version that Discord.Net uses to communicate with KOOK's + /// An representing the API version that Kook.Net uses to communicate with KOOK's /// voice server. /// public const int VoiceAPIVersion = 1; @@ -160,7 +160,7 @@ public class KookConfig /// /// This property is mutually exclusive with . /// - public Func DefaultRatelimitCallback { get; set; } + public Func? DefaultRatelimitCallback { get; set; } /// /// Gets or sets the minimum log level severity that will be sent to the Log event. diff --git a/src/Kook.Net.Core/Logging/LogManager.cs b/src/Kook.Net.Core/Logging/LogManager.cs index 76e6072e..b02ff8d3 100644 --- a/src/Kook.Net.Core/Logging/LogManager.cs +++ b/src/Kook.Net.Core/Logging/LogManager.cs @@ -19,7 +19,7 @@ public LogManager(LogSeverity minSeverity) ClientLogger = new Logger(this, "Kook"); } - public async Task LogAsync(LogSeverity severity, string source, Exception ex) + public async Task LogAsync(LogSeverity severity, string source, Exception? ex) { try { @@ -32,7 +32,7 @@ public async Task LogAsync(LogSeverity severity, string source, Exception ex) } } - public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) + public async Task LogAsync(LogSeverity severity, string source, string message, Exception? ex = null) { try { @@ -45,12 +45,13 @@ public async Task LogAsync(LogSeverity severity, string source, string message, } } - public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) + public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception? ex = null) { try { if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)) + .ConfigureAwait(false); } catch { @@ -59,57 +60,53 @@ public async Task LogAsync(LogSeverity severity, string source, FormattableStrin } - public Task ErrorAsync(string source, Exception ex) - => LogAsync(LogSeverity.Error, source, ex); + public Task ErrorAsync(string source, Exception? ex) => LogAsync(LogSeverity.Error, source, ex); - public Task ErrorAsync(string source, string message, Exception ex = null) - => LogAsync(LogSeverity.Error, source, message, ex); + public Task ErrorAsync(string source, string message, Exception? ex = null) => + LogAsync(LogSeverity.Error, source, message, ex); - public Task ErrorAsync(string source, FormattableString message, Exception ex = null) - => LogAsync(LogSeverity.Error, source, message, ex); + public Task ErrorAsync(string source, FormattableString message, Exception? ex = null) => + LogAsync(LogSeverity.Error, source, message, ex); - public Task WarningAsync(string source, Exception ex) - => LogAsync(LogSeverity.Warning, source, ex); + public Task WarningAsync(string source, Exception? ex) => LogAsync(LogSeverity.Warning, source, ex); - public Task WarningAsync(string source, string message, Exception ex = null) - => LogAsync(LogSeverity.Warning, source, message, ex); + public Task WarningAsync(string source, string message, Exception? ex = null) => + LogAsync(LogSeverity.Warning, source, message, ex); - public Task WarningAsync(string source, FormattableString message, Exception ex = null) - => LogAsync(LogSeverity.Warning, source, message, ex); + public Task WarningAsync(string source, FormattableString message, Exception? ex = null) => + LogAsync(LogSeverity.Warning, source, message, ex); - public Task InfoAsync(string source, Exception ex) - => LogAsync(LogSeverity.Info, source, ex); + public Task InfoAsync(string source, Exception? ex) => LogAsync(LogSeverity.Info, source, ex); - public Task InfoAsync(string source, string message, Exception ex = null) - => LogAsync(LogSeverity.Info, source, message, ex); + public Task InfoAsync(string source, string message, Exception? ex = null) => + LogAsync(LogSeverity.Info, source, message, ex); - public Task InfoAsync(string source, FormattableString message, Exception ex = null) - => LogAsync(LogSeverity.Info, source, message, ex); + public Task InfoAsync(string source, FormattableString message, Exception? ex = null) => + LogAsync(LogSeverity.Info, source, message, ex); - public Task VerboseAsync(string source, Exception ex) - => LogAsync(LogSeverity.Verbose, source, ex); + public Task VerboseAsync(string source, Exception? ex) => LogAsync(LogSeverity.Verbose, source, ex); - public Task VerboseAsync(string source, string message, Exception ex = null) - => LogAsync(LogSeverity.Verbose, source, message, ex); + public Task VerboseAsync(string source, string message, Exception? ex = null) => + LogAsync(LogSeverity.Verbose, source, message, ex); - public Task VerboseAsync(string source, FormattableString message, Exception ex = null) - => LogAsync(LogSeverity.Verbose, source, message, ex); + public Task VerboseAsync(string source, FormattableString message, Exception? ex = null) => + LogAsync(LogSeverity.Verbose, source, message, ex); - public Task DebugAsync(string source, Exception ex) - => LogAsync(LogSeverity.Debug, source, ex); + public Task DebugAsync(string source, Exception? ex) => LogAsync(LogSeverity.Debug, source, ex); - public Task DebugAsync(string source, string message, Exception ex = null) - => LogAsync(LogSeverity.Debug, source, message, ex); + public Task DebugAsync(string source, string message, Exception? ex = null) => + LogAsync(LogSeverity.Debug, source, message, ex); - public Task DebugAsync(string source, FormattableString message, Exception ex = null) - => LogAsync(LogSeverity.Debug, source, message, ex); + public Task DebugAsync(string source, FormattableString message, Exception? ex = null) => + LogAsync(LogSeverity.Debug, source, message, ex); public Logger CreateLogger(string name) => new(this, name); public async Task WriteInitialLog() => - await ClientLogger.InfoAsync($"Kook.Net v{KookConfig.Version} (API v{KookConfig.APIVersion})").ConfigureAwait(false); + await ClientLogger.InfoAsync($"Kook.Net v{KookConfig.Version} (API v{KookConfig.APIVersion})") + .ConfigureAwait(false); } diff --git a/src/Kook.Net.Core/Logging/LogMessage.cs b/src/Kook.Net.Core/Logging/LogMessage.cs index c63ca0f7..562bd809 100644 --- a/src/Kook.Net.Core/Logging/LogMessage.cs +++ b/src/Kook.Net.Core/Logging/LogMessage.cs @@ -5,7 +5,7 @@ namespace Kook; /// /// Provides a message object used for logging purposes. /// -public struct LogMessage +public readonly struct LogMessage { /// /// Gets the severity of the log entry. @@ -29,7 +29,7 @@ public struct LogMessage /// /// A string containing the message of this log entry. /// - public string Message { get; } + public string? Message { get; } /// /// Gets the exception of this log entry. @@ -37,7 +37,7 @@ public struct LogMessage /// /// A object associated with an incident; otherwise null. /// - public Exception Exception { get; } + public Exception? Exception { get; } /// /// Initializes a new struct with the severity, source, message of the event, and @@ -47,7 +47,7 @@ public struct LogMessage /// The source of the event. /// The message of the event. /// The exception of the event. - public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) + public LogMessage(LogSeverity severity, string source, string? message, Exception? exception = null) { Severity = severity; Source = source; @@ -70,15 +70,20 @@ public LogMessage(LogSeverity severity, string source, string message, Exception /// The kind of timestamp to use. /// The amount of padding to use for the source. /// A string representation of this log message. - public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, + public string ToString(StringBuilder? builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11) { - string sourceName = Source; - string message = Message; - string exMessage = fullException ? Exception?.ToString() : Exception?.Message; + string? exMessage = fullException ? Exception?.ToString() : Exception?.Message; int maxLength = - 1 + (prependTimestamp ? 8 : 0) + 1 + (padSource ?? (sourceName?.Length ?? 0)) + 1 + (message?.Length ?? 0) + (exMessage?.Length ?? 0) + 3; + 1 + + (prependTimestamp ? 8 : 0) + + 1 + + (padSource ?? (Source?.Length ?? 0)) + + 1 + + (Message?.Length ?? 0) + + (exMessage?.Length ?? 0) + + 3; if (builder == null) builder = new StringBuilder(maxLength); @@ -90,42 +95,37 @@ public string ToString(StringBuilder builder = null, bool fullException = true, if (prependTimestamp) { - DateTime now; - if (timestampKind == DateTimeKind.Utc) - now = DateTime.UtcNow; - else - now = DateTime.Now; - - string format = "HH:mm:ss"; - builder.Append(now.ToString(format)); + DateTime now = timestampKind == DateTimeKind.Utc + ? DateTime.UtcNow + : DateTime.Now; + + builder.Append(now.ToString("HH:mm:ss")); builder.Append(' '); } - if (sourceName != null) + if (Source != null) { if (padSource.HasValue) { - if (sourceName.Length < padSource.Value) + if (Source.Length < padSource.Value) { - builder.Append(sourceName); - builder.Append(' ', padSource.Value - sourceName.Length); + builder.Append(Source); + builder.Append(' ', padSource.Value - Source.Length); } - else if (sourceName.Length > padSource.Value) - builder.Append(sourceName.Substring(0, padSource.Value)); + else if (Source.Length > padSource.Value) + builder.Append(Source[..padSource.Value]); else - builder.Append(sourceName); + builder.Append(Source); } builder.Append(' '); } - if (!string.IsNullOrEmpty(Message)) - for (int i = 0; i < message.Length; i++) - { - //Strip control chars - char c = message[i]; - if (!char.IsControl(c)) builder.Append(c); - } + if (Message != null && !string.IsNullOrEmpty(Message)) + { + foreach (char c in Message.Where(c => !char.IsControl(c))) + builder.Append(c); + } if (exMessage != null) { diff --git a/src/Kook.Net.Core/Logging/Logger.cs b/src/Kook.Net.Core/Logging/Logger.cs index df02aa22..90d19d54 100644 --- a/src/Kook.Net.Core/Logging/Logger.cs +++ b/src/Kook.Net.Core/Logging/Logger.cs @@ -5,6 +5,7 @@ internal class Logger private readonly LogManager _manager; public string Name { get; } + public LogSeverity Level => _manager.Level; public Logger(LogManager manager, string name) @@ -13,62 +14,56 @@ public Logger(LogManager manager, string name) Name = name; } - public Task LogAsync(LogSeverity severity, Exception exception = null) - => _manager.LogAsync(severity, Name, exception); + public Task LogAsync(LogSeverity severity, Exception? exception = null) => + _manager.LogAsync(severity, Name, exception); - public Task LogAsync(LogSeverity severity, string message, Exception exception = null) - => _manager.LogAsync(severity, Name, message, exception); + public Task LogAsync(LogSeverity severity, string message, Exception? exception = null) => + _manager.LogAsync(severity, Name, message, exception); - public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) - => _manager.LogAsync(severity, Name, message, exception); + public Task LogAsync(LogSeverity severity, FormattableString message, Exception? exception = null) => + _manager.LogAsync(severity, Name, message, exception); - public Task ErrorAsync(Exception exception) - => _manager.ErrorAsync(Name, exception); + public Task ErrorAsync(Exception? exception) => _manager.ErrorAsync(Name, exception); - public Task ErrorAsync(string message, Exception exception = null) - => _manager.ErrorAsync(Name, message, exception); + public Task ErrorAsync(string message, Exception? exception = null) => + _manager.ErrorAsync(Name, message, exception); - public Task ErrorAsync(FormattableString message, Exception exception = null) - => _manager.ErrorAsync(Name, message, exception); + public Task ErrorAsync(FormattableString message, Exception? exception = null) => + _manager.ErrorAsync(Name, message, exception); - public Task WarningAsync(Exception exception) - => _manager.WarningAsync(Name, exception); + public Task WarningAsync(Exception? exception) => _manager.WarningAsync(Name, exception); - public Task WarningAsync(string message, Exception exception = null) - => _manager.WarningAsync(Name, message, exception); + public Task WarningAsync(string message, Exception? exception = null) => + _manager.WarningAsync(Name, message, exception); - public Task WarningAsync(FormattableString message, Exception exception = null) - => _manager.WarningAsync(Name, message, exception); + public Task WarningAsync(FormattableString message, Exception? exception = null) => + _manager.WarningAsync(Name, message, exception); - public Task InfoAsync(Exception exception) - => _manager.InfoAsync(Name, exception); + public Task InfoAsync(Exception? exception) => _manager.InfoAsync(Name, exception); - public Task InfoAsync(string message, Exception exception = null) - => _manager.InfoAsync(Name, message, exception); + public Task InfoAsync(string message, Exception? exception = null) => _manager.InfoAsync(Name, message, exception); - public Task InfoAsync(FormattableString message, Exception exception = null) - => _manager.InfoAsync(Name, message, exception); + public Task InfoAsync(FormattableString message, Exception? exception = null) => + _manager.InfoAsync(Name, message, exception); - public Task VerboseAsync(Exception exception) - => _manager.VerboseAsync(Name, exception); + public Task VerboseAsync(Exception? exception) => _manager.VerboseAsync(Name, exception); - public Task VerboseAsync(string message, Exception exception = null) - => _manager.VerboseAsync(Name, message, exception); + public Task VerboseAsync(string message, Exception? exception = null) => + _manager.VerboseAsync(Name, message, exception); - public Task VerboseAsync(FormattableString message, Exception exception = null) - => _manager.VerboseAsync(Name, message, exception); + public Task VerboseAsync(FormattableString message, Exception? exception = null) => + _manager.VerboseAsync(Name, message, exception); - public Task DebugAsync(Exception exception) - => _manager.DebugAsync(Name, exception); + public Task DebugAsync(Exception? exception) => _manager.DebugAsync(Name, exception); - public Task DebugAsync(string message, Exception exception = null) - => _manager.DebugAsync(Name, message, exception); + public Task DebugAsync(string message, Exception? exception = null) => + _manager.DebugAsync(Name, message, exception); - public Task DebugAsync(FormattableString message, Exception exception = null) - => _manager.DebugAsync(Name, message, exception); + public Task DebugAsync(FormattableString message, Exception? exception = null) => + _manager.DebugAsync(Name, message, exception); } diff --git a/src/Kook.Net.Core/Net/BucketId.cs b/src/Kook.Net.Core/Net/BucketId.cs index 432ef592..0f85d02c 100644 --- a/src/Kook.Net.Core/Net/BucketId.cs +++ b/src/Kook.Net.Core/Net/BucketId.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; #if NET462 using System.Net.Http; #endif @@ -7,17 +8,17 @@ namespace Kook.Net; /// /// Represents a ratelimit bucket. /// -public class BucketId : IEquatable +public sealed class BucketId : IEquatable { /// /// Gets the http method used to make the request if available. /// - public HttpMethod HttpMethod { get; } + public HttpMethod? HttpMethod { get; } /// /// Gets the endpoint that is going to be requested if available. /// - public string Endpoint { get; } + public string? Endpoint { get; } /// /// Gets the major parameters of the route. @@ -30,14 +31,14 @@ public class BucketId : IEquatable /// /// The hash is provided by Kook to group ratelimits. /// - public string BucketHash { get; } + public string? BucketHash { get; } /// /// Gets if this bucket is a hash type. /// public bool IsHashBucket => BucketHash != null; - private BucketId(HttpMethod httpMethod, string endpoint, IEnumerable> majorParameters, string bucketHash) + private BucketId(HttpMethod? httpMethod, string? endpoint, IEnumerable> majorParameters, string? bucketHash) { HttpMethod = httpMethod; Endpoint = endpoint; @@ -56,7 +57,7 @@ private BucketId(HttpMethod httpMethod, string endpoint, IEnumerable based on the /// and the with the provided data. /// - public static BucketId Create(HttpMethod httpMethod, string endpoint, Dictionary majorParams) + public static BucketId Create(HttpMethod? httpMethod, string? endpoint, Dictionary? majorParams) { Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint)); majorParams ??= new Dictionary(); @@ -86,8 +87,9 @@ public static BucketId Create(string hash, BucketId oldBucket) /// /// A string that defines this bucket as a hash based one. /// - public string GetBucketHash() - => IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null; + public string? GetBucketHash() => IsHashBucket + ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" + : null; /// /// Gets the string that will define this bucket as an endpoint based one. @@ -95,30 +97,33 @@ public string GetBucketHash() /// /// A string that defines this bucket as an endpoint based one. /// - public string GetUniqueEndpoint() - => HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint; + public string? GetUniqueEndpoint() => HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint; /// - public override bool Equals(object obj) - => Equals(obj as BucketId); + public override int GetHashCode() => IsHashBucket + ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() + : (HttpMethod, Endpoint).GetHashCode(); /// - public override int GetHashCode() - => IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode(); + public override string? ToString() => GetBucketHash() ?? GetUniqueEndpoint(); /// - public override string ToString() - => GetBucketHash() ?? GetUniqueEndpoint(); + public override bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is not BucketId bucketId) + return false; + return Equals(bucketId); + } /// - public bool Equals(BucketId other) + public bool Equals(BucketId? other) { - if (other is null) return false; - - if (ReferenceEquals(this, other)) return true; - - if (GetType() != other.GetType()) return false; - + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (GetType() != other.GetType()) + return false; return ToString() == other.ToString(); } } diff --git a/src/Kook.Net.Core/Net/HttpException.cs b/src/Kook.Net.Core/Net/HttpException.cs index 74a0c2d4..4465951f 100644 --- a/src/Kook.Net.Core/Net/HttpException.cs +++ b/src/Kook.Net.Core/Net/HttpException.cs @@ -27,7 +27,7 @@ public class HttpException : Exception /// /// Gets the reason of the exception. /// - public string Reason { get; } + public string? Reason { get; } /// /// Gets the request object used to send the request. @@ -47,8 +47,8 @@ public class HttpException : Exception /// The Kook status code returned. /// The reason behind the exception. /// A collection of json errors describing what went wrong with the request. - public HttpException(HttpStatusCode httpCode, IRequest request, KookErrorCode? kookCode = null, string reason = null, - KookJsonError[] errors = null) + public HttpException(HttpStatusCode httpCode, IRequest request, KookErrorCode? kookCode = null, string? reason = null, + KookJsonError[]? errors = null) : base(CreateMessage(httpCode, (int?)kookCode, reason)) { HttpCode = httpCode; @@ -58,24 +58,13 @@ public HttpException(HttpStatusCode httpCode, IRequest request, KookErrorCode? k Errors = errors?.ToImmutableArray() ?? ImmutableArray.Empty; } - private static string CreateMessage(HttpStatusCode httpCode, int? kookCode = null, string reason = null) + private static string CreateMessage(HttpStatusCode httpCode, int? kookCode = null, string? reason = null) { - string msg; - if (kookCode != null && kookCode != 0) - { - if (reason != null) - msg = $"The server responded with error {(int)kookCode}: {reason}"; - else - msg = $"The server responded with error {(int)kookCode}: {httpCode}"; - } - else - { - if (reason != null) - msg = $"The server responded with error {(int)httpCode}: {reason}"; - else - msg = $"The server responded with error {(int)httpCode}: {httpCode}"; - } - - return msg; + int closeCode = kookCode.HasValue && kookCode != 0 + ? kookCode.Value + : (int) httpCode; + return reason != null + ? $"The server responded with error {closeCode}: {reason}" + : $"The server responded with error {closeCode}: {httpCode}"; } } diff --git a/src/Kook.Net.Core/Net/RateLimitedException.cs b/src/Kook.Net.Core/Net/RateLimitedException.cs index 2fd45e93..b9ccdf24 100644 --- a/src/Kook.Net.Core/Net/RateLimitedException.cs +++ b/src/Kook.Net.Core/Net/RateLimitedException.cs @@ -15,6 +15,8 @@ public class RateLimitedException : TimeoutException /// sent. /// public RateLimitedException(IRequest request) - : base("You are being rate limited.") => + : base("You are being rate limited.") + { Request = request; + } } diff --git a/src/Kook.Net.Core/Net/Rest/IRateLimitInfo.cs b/src/Kook.Net.Core/Net/Rest/IRateLimitInfo.cs index 80c51dbb..467dca60 100644 --- a/src/Kook.Net.Core/Net/Rest/IRateLimitInfo.cs +++ b/src/Kook.Net.Core/Net/Rest/IRateLimitInfo.cs @@ -6,7 +6,7 @@ namespace Kook; public interface IRateLimitInfo { /// - /// Gets whether or not this ratelimit info is global. + /// Gets whether this ratelimit info is global. /// bool IsGlobal { get; } @@ -38,7 +38,7 @@ public interface IRateLimitInfo /// /// Gets a unique string denoting the rate limit being encountered (non-inclusive of major parameters in the route path). /// - string Bucket { get; } + string? Bucket { get; } /// /// Gets the amount of lag for the request. This is used to denote the precise time of when the ratelimit expires. diff --git a/src/Kook.Net.Core/Net/Rest/IRestClient.cs b/src/Kook.Net.Core/Net/Rest/IRestClient.cs index 68b7c986..4ae78c02 100644 --- a/src/Kook.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Kook.Net.Core/Net/Rest/IRestClient.cs @@ -14,7 +14,7 @@ public interface IRestClient : IDisposable /// /// The field name of the header. /// The value of the header. - void SetHeader(string key, string value); + void SetHeader(string key, string? value); /// /// Sets the cancellation token for this client. @@ -31,8 +31,9 @@ public interface IRestClient : IDisposable /// The audit log reason. /// Additional headers to be sent with the request. /// A task that represents an asynchronous send operation. The task result contains the REST response of the request. - Task SendAsync(HttpMethod method, string endpoint, CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null); + Task SendAsync(HttpMethod method, string endpoint, + CancellationToken cancellationToken, string? reason = null, + IEnumerable>>? requestHeaders = null); /// /// Sends a REST request with a JSON body. @@ -44,8 +45,9 @@ Task SendAsync(HttpMethod method, string endpoint, CancellationTok /// The audit log reason. /// Additional headers to be sent with the request. /// A task that represents an asynchronous send operation. The task result contains the REST response of the request. - Task SendAsync(HttpMethod method, string endpoint, string json, CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null); + Task SendAsync(HttpMethod method, string endpoint, string json, + CancellationToken cancellationToken, string? reason = null, + IEnumerable>>? requestHeaders = null); /// /// Sends a REST request with multipart parameters. @@ -58,6 +60,6 @@ Task SendAsync(HttpMethod method, string endpoint, string json, Ca /// Additional headers to be sent with the request. /// A task that represents an asynchronous send operation. The task result contains the REST response of the request. Task SendAsync(HttpMethod method, string endpoint, IReadOnlyDictionary multipartParams, - CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null); + CancellationToken cancellationToken, string? reason = null, + IEnumerable>>? requestHeaders = null); } diff --git a/src/Kook.Net.Core/Net/Rest/RestResponse.cs b/src/Kook.Net.Core/Net/Rest/RestResponse.cs index af2c49e0..d5d961a2 100644 --- a/src/Kook.Net.Core/Net/Rest/RestResponse.cs +++ b/src/Kook.Net.Core/Net/Rest/RestResponse.cs @@ -16,7 +16,7 @@ public struct RestResponse /// /// Gets the headers of the response. /// - public Dictionary Headers { get; } + public Dictionary Headers { get; } /// /// Gets the stream of the response. @@ -26,9 +26,10 @@ public struct RestResponse /// /// Gets the media type header of the response. /// - public MediaTypeHeaderValue MediaTypeHeader { get; } + public MediaTypeHeaderValue? MediaTypeHeader { get; } - internal RestResponse(HttpStatusCode statusCode, Dictionary headers, Stream stream, MediaTypeHeaderValue mediaTypeHeader) + internal RestResponse(HttpStatusCode statusCode, Dictionary headers, Stream stream, + MediaTypeHeaderValue? mediaTypeHeader) { StatusCode = statusCode; Headers = headers; diff --git a/src/Kook.Net.Core/Net/Udp/IUdpSocket.cs b/src/Kook.Net.Core/Net/Udp/IUdpSocket.cs index 97ad438f..67d8fe69 100644 --- a/src/Kook.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Kook.Net.Core/Net/Udp/IUdpSocket.cs @@ -8,7 +8,7 @@ public interface IUdpSocket : IDisposable /// /// Fired when a datagram is received. /// - event Func ReceivedDatagram; + event Func? ReceivedDatagram; /// /// Gets the port of the socket. diff --git a/src/Kook.Net.Core/Net/WebSocketClosedException.cs b/src/Kook.Net.Core/Net/WebSocketClosedException.cs index 82310873..6ed8ad32 100644 --- a/src/Kook.Net.Core/Net/WebSocketClosedException.cs +++ b/src/Kook.Net.Core/Net/WebSocketClosedException.cs @@ -9,22 +9,20 @@ public class WebSocketClosedException : Exception /// Gets the close code sent by Kook. /// /// - /// A - /// close code - /// from Kook. + /// A close code from Kook. See https://developer.kookapp.cn/doc/websocket /// - public int CloseCode { get; } + public int? CloseCode { get; } /// /// Gets the reason of the interruption. /// - public string Reason { get; } + public string? Reason { get; } /// /// Initializes a new instance of the using a Kook close code /// and an optional reason. /// - public WebSocketClosedException(int closeCode, string reason = null) + public WebSocketClosedException(int? closeCode, string? reason = null) : base($"The server sent close {closeCode}{(reason != null ? $": \"{reason}\"" : "")}") { CloseCode = closeCode; diff --git a/src/Kook.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Kook.Net.Core/Net/WebSockets/IWebSocketClient.cs index 033b7356..77e370a8 100644 --- a/src/Kook.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Kook.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -8,17 +8,17 @@ public interface IWebSocketClient : IDisposable /// /// Fired when a binary message is received. /// - event Func BinaryMessage; + event Func? BinaryMessage; /// /// Fired when a text message is received. /// - event Func TextMessage; + event Func? TextMessage; /// /// Fired when the WebSocket connection is closed. /// - event Func Closed; + event Func? Closed; /// /// Sets a header to be sent with the future requests. diff --git a/src/Kook.Net.Core/RequestOptions.cs b/src/Kook.Net.Core/RequestOptions.cs index 50f26ea6..f57470b6 100644 --- a/src/Kook.Net.Core/RequestOptions.cs +++ b/src/Kook.Net.Core/RequestOptions.cs @@ -45,31 +45,29 @@ public class RequestOptions /// Gets or sets the reason that will be written to the guild's audit log if applicable. This may not apply /// to all actions. /// - public string AuditLogReason { get; set; } + public string? AuditLogReason { get; set; } /// /// Gets or sets the callback to execute regarding ratelimits for this request. /// - public Func RatelimitCallback { get; set; } + public Func? RatelimitCallback { get; set; } internal bool IgnoreState { get; set; } - internal BucketId BucketId { get; set; } + internal BucketId? BucketId { get; set; } internal bool IsClientBucket { get; set; } internal bool IsGatewayBucket { get; set; } - internal IDictionary> RequestHeaders { get; } + internal IDictionary>? RequestHeaders { get; } - internal static RequestOptions CreateOrClone(RequestOptions options) - { - if (options == null) - return new RequestOptions(); - else - return options.Clone(); - } + internal static RequestOptions CreateOrClone(RequestOptions? options) => + options == null + ? new RequestOptions() + : options.Clone(); internal void ExecuteRatelimitCallback(IRateLimitInfo info) { - if (RatelimitCallback != null) _ = Task.Run(async () => { await RatelimitCallback(info); }); + if (RatelimitCallback != null) + _ = Task.Run(async () => await RatelimitCallback(info).ConfigureAwait(false), CancellationToken.None); } /// @@ -86,5 +84,5 @@ public RequestOptions() /// Memberwise clones this object. /// /// A cloned object. - public RequestOptions Clone() => MemberwiseClone() as RequestOptions; + public RequestOptions Clone() => (RequestOptions) MemberwiseClone(); } diff --git a/src/Kook.Net.Core/Utils/AsyncEvent.cs b/src/Kook.Net.Core/Utils/AsyncEvent.cs index d73915a0..296613a8 100644 --- a/src/Kook.Net.Core/Utils/AsyncEvent.cs +++ b/src/Kook.Net.Core/Utils/AsyncEvent.cs @@ -6,12 +6,13 @@ internal class AsyncEvent where T : class { private readonly object _subLock = new(); - internal ImmutableArray _subscriptions; + internal ImmutableArray _subscriptions = []; + // ReSharper disable once InconsistentlySynchronizedField public bool HasSubscribers => _subscriptions.Length != 0; - public IReadOnlyList Subscriptions => _subscriptions; - public AsyncEvent() => _subscriptions = ImmutableArray.Create(); + // ReSharper disable once InconsistentlySynchronizedField + public IReadOnlyList Subscriptions => _subscriptions; public void Add(T subscriber) { @@ -37,43 +38,42 @@ internal static class EventExtensions public static async Task InvokeAsync(this AsyncEvent> eventHandler) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke().ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke().ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg).ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke(arg).ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke(arg1, arg2).ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke(arg1, arg2, arg3).ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, - T4 arg4, T5 arg5) + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { IReadOnlyList> subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + foreach (Func x in subscribers) + await x.Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Core/Utils/Cacheable.cs b/src/Kook.Net.Core/Utils/Cacheable.cs index 77f444a0..99a5d9b8 100644 --- a/src/Kook.Net.Core/Utils/Cacheable.cs +++ b/src/Kook.Net.Core/Utils/Cacheable.cs @@ -8,8 +8,10 @@ namespace Kook; /// /// The type of entity that is cached. /// The type of this entity's ID. -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public struct Cacheable +#if DEBUG +[DebuggerDisplay("{DebuggerDisplay,nq}")] +#endif +public readonly struct Cacheable where TEntity : IEntity where TId : IEquatable { @@ -30,11 +32,11 @@ public struct Cacheable /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is /// null. /// - public TEntity Value { get; } + public TEntity? Value { get; } - private Func> DownloadFunc { get; } + private Func> DownloadFunc { get; } - internal Cacheable(TEntity value, TId id, bool hasValue, Func> downloadFunc) + internal Cacheable(TEntity? value, TId id, bool hasValue, Func> downloadFunc) { Value = value; Id = id; @@ -48,10 +50,10 @@ internal Cacheable(TEntity value, TId id, bool hasValue, Func> dow /// Thrown when used from a user account. /// Thrown when the entity is deleted. /// - /// A task that represents the asynchronous download operation. The task result contains the downloaded - /// entity. + /// A task that represents the asynchronous download operation. The task result contains + /// the downloaded entity. /// - public async Task DownloadAsync() => await DownloadFunc().ConfigureAwait(false); + public async Task DownloadAsync() => await DownloadFunc().ConfigureAwait(false); /// /// Returns the cached entity if it exists; otherwise downloads it. @@ -62,11 +64,13 @@ internal Cacheable(TEntity value, TId id, bool hasValue, Func> dow /// A task that represents the asynchronous operation that attempts to get the entity via cache or to /// download the entity. The task result contains the downloaded entity. /// - public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); +#if DEBUG private string DebuggerDisplay => HasValue && Value != null ? $"{Value.GetType().GetProperty("DebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(Value) ?? Value.ToString()} (Cacheable)" : $"{Id} (Cacheable, {typeof(TEntity).Name})"; +#endif } /// @@ -76,8 +80,10 @@ internal Cacheable(TEntity value, TId id, bool hasValue, Func> dow /// The type of entity that can be downloaded. /// The common type of and . /// The type of the corresponding entity's ID. -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public struct Cacheable +#if DEBUG +[DebuggerDisplay("{DebuggerDisplay,nq}")] +#endif +public readonly struct Cacheable where TCachedEntity : IEntity, TRelationship where TDownloadableEntity : IEntity, TRelationship where TId : IEquatable @@ -99,11 +105,11 @@ public struct Cacheable /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is /// null. /// - public TCachedEntity Value { get; } + public TCachedEntity? Value { get; } - private Func> DownloadFunc { get; } + private Func> DownloadFunc { get; } - internal Cacheable(TCachedEntity value, TId id, bool hasValue, Func> downloadFunc) + internal Cacheable(TCachedEntity? value, TId id, bool hasValue, Func> downloadFunc) { Value = value; Id = id; @@ -120,7 +126,7 @@ internal Cacheable(TCachedEntity value, TId id, bool hasValue, Func - public async Task DownloadAsync() => await DownloadFunc().ConfigureAwait(false); + public async Task DownloadAsync() => await DownloadFunc().ConfigureAwait(false); /// /// Returns the cached entity if it exists; otherwise downloads it. @@ -131,8 +137,11 @@ internal Cacheable(TCachedEntity value, TId id, bool hasValue, Func - public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); + +#if DEBUG private string DebuggerDisplay => HasValue && Value != null ? $"{Value.GetType().GetProperty("DebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(Value) ?? Value.ToString()} (Cacheable)" : $"{Id} (Cacheable, {typeof(TRelationship).Name})"; +#endif } diff --git a/src/Kook.Net.Core/Utils/ConcurrentHashSet.cs b/src/Kook.Net.Core/Utils/ConcurrentHashSet.cs index 84843b92..92630f6f 100644 --- a/src/Kook.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Kook.Net.Core/Utils/ConcurrentHashSet.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Kook; @@ -8,22 +9,23 @@ namespace Kook; //Copyright (c) .NET Foundation and Contributors internal static class ConcurrentHashSet { - private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; - private static volatile int s_processorCount; - private static volatile int s_lastProcessorCountRefreshTicks; + private const int ProcessorCountRefreshIntervalMs = 30000; + private static volatile int ProcessorCount; + private static volatile int LastProcessorCountRefreshTicks; public static int DefaultConcurrencyLevel { get { int now = Environment.TickCount; - if (s_processorCount == 0 || now - s_lastProcessorCountRefreshTicks >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS) + if (ProcessorCount == 0 + || now - LastProcessorCountRefreshTicks >= ProcessorCountRefreshIntervalMs) { - s_processorCount = Environment.ProcessorCount; - s_lastProcessorCountRefreshTicks = now; + ProcessorCount = Environment.ProcessorCount; + LastProcessorCountRefreshTicks = now; } - return s_processorCount; + return ProcessorCount; } } } @@ -88,13 +90,11 @@ public int Count get { int count = 0; - int acquiredLocks = 0; try { AcquireAllLocks(ref acquiredLocks); - - for (int i = 0; i < _tables._countPerLock.Length; i++) count += _tables._countPerLock[i]; + count += _tables._countPerLock.Sum(); } finally { @@ -114,10 +114,8 @@ public bool IsEmpty { // Acquire all locks AcquireAllLocks(ref acquiredLocks); - - for (int i = 0; i < _tables._countPerLock.Length; i++) - if (_tables._countPerLock[i] != 0) - return false; + if (Array.Exists(_tables._countPerLock, c => c != 0)) + return false; } finally { @@ -136,11 +134,11 @@ public ReadOnlyCollection Values try { AcquireAllLocks(ref locksAcquired); - List values = new(); + List values = []; - for (int i = 0; i < _tables._buckets.Length; i++) + foreach (Node node in _tables._buckets) { - Node current = _tables._buckets[i]; + Node current = node; while (current != null) { values.Add(current._value); @@ -181,7 +179,8 @@ public ConcurrentHashSet(IEqualityComparer comparer) public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) : this(comparer) { - if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (collection == null) + throw new ArgumentNullException(nameof(collection)); InitializeFromCollection(collection); } @@ -192,9 +191,11 @@ public ConcurrentHashSet(IEnumerable collection, IEqualityComparer compare public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) : this(concurrencyLevel, DefaultCapacity, false, comparer) { - if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (collection == null) + throw new ArgumentNullException(nameof(collection)); - if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + if (comparer == null) + throw new ArgumentNullException(nameof(comparer)); InitializeFromCollection(collection); } @@ -206,16 +207,21 @@ public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer comparer) { - if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel)); + if (concurrencyLevel < 1) + throw new ArgumentOutOfRangeException(nameof(concurrencyLevel)); - if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity)); - if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + if (comparer == null) + throw new ArgumentNullException(nameof(comparer)); - if (capacity < concurrencyLevel) capacity = concurrencyLevel; + if (capacity < concurrencyLevel) + capacity = concurrencyLevel; object[] locks = new object[concurrencyLevel]; - for (int i = 0; i < locks.Length; i++) locks[i] = new object(); + for (int i = 0; i < locks.Length; i++) + locks[i] = new object(); int[] countPerLock = new int[locks.Length]; Node[] buckets = new Node[capacity]; @@ -230,18 +236,22 @@ private void InitializeFromCollection(IEnumerable collection) { foreach (T value in collection) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) + throw new ArgumentNullException("key"); - if (!TryAddInternal(value, _comparer.GetHashCode(value), false)) throw new ArgumentException(); + if (!TryAddInternal(value, _comparer.GetHashCode(value), false)) + throw new ArgumentException(); } - if (_budget == 0) _budget = _tables._buckets.Length / _tables._locks.Length; + if (_budget == 0) + _budget = _tables._buckets.Length / _tables._locks.Length; } /// is null public bool ContainsKey(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) + throw new ArgumentNullException("key"); return ContainsKeyInternal(value, _comparer.GetHashCode(value)); } @@ -249,15 +259,12 @@ public bool ContainsKey(T value) private bool ContainsKeyInternal(T value, int hashcode) { Tables tables = _tables; - int bucketNo = GetBucket(hashcode, tables._buckets.Length); - Node n = Volatile.Read(ref tables._buckets[bucketNo]); - while (n != null) { - if (hashcode == n._hashcode && _comparer.Equals(n._value, value)) return true; - + if (hashcode == n._hashcode && _comparer.Equals(n._value, value)) + return true; n = n._next; } @@ -267,7 +274,8 @@ private bool ContainsKeyInternal(T value, int hashcode) /// is null public bool TryAdd(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) + throw new ArgumentNullException("key"); return TryAddInternal(value, _comparer.GetHashCode(value), true); } @@ -283,14 +291,17 @@ private bool TryAddInternal(T value, int hashcode, bool acquireLock) bool lockTaken = false; try { - if (acquireLock) Monitor.Enter(tables._locks[lockNo], ref lockTaken); + if (acquireLock) + Monitor.Enter(tables._locks[lockNo], ref lockTaken); - if (tables != _tables) continue; + if (tables != _tables) + continue; - Node prev = null; + Node? prev = null; for (Node node = tables._buckets[bucketNo]; node != null; node = node._next) { - if (hashcode == node._hashcode && _comparer.Equals(node._value, value)) return false; + if (hashcode == node._hashcode && _comparer.Equals(node._value, value)) + return false; prev = node; } @@ -305,10 +316,12 @@ private bool TryAddInternal(T value, int hashcode, bool acquireLock) } finally { - if (lockTaken) Monitor.Exit(tables._locks[lockNo]); + if (lockTaken) + Monitor.Exit(tables._locks[lockNo]); } - if (resizeDesired) GrowTable(tables); + if (resizeDesired) + GrowTable(tables); return true; } @@ -317,12 +330,13 @@ private bool TryAddInternal(T value, int hashcode, bool acquireLock) /// is null public bool TryRemove(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) + throw new ArgumentNullException("key"); return TryRemoveInternal(value); } - private bool TryRemoveInternal(T value) + private bool TryRemoveInternal([DisallowNull] T value) { int hashcode = _comparer.GetHashCode(value); while (true) @@ -334,7 +348,7 @@ private bool TryRemoveInternal(T value) { if (tables != _tables) continue; - Node prev = null; + Node? prev = null; for (Node curr = tables._buckets[bucketNo]; curr != null; curr = curr._next) { if (hashcode == curr._hashcode && _comparer.Equals(curr._value, value)) @@ -353,7 +367,7 @@ private bool TryRemoveInternal(T value) } } - value = default(T); + value = default!; return false; } } @@ -400,15 +414,18 @@ private void GrowTable(Tables tables) try { AcquireLocks(0, 1, ref locksAcquired); - if (tables != _tables) return; + if (tables != _tables) + return; long approxCount = 0; - for (int i = 0; i < tables._countPerLock.Length; i++) approxCount += tables._countPerLock[i]; + foreach (int x in tables._countPerLock) + approxCount += x; if (approxCount < tables._buckets.Length / 4) { _budget = 2 * _budget; - if (_budget < 0) _budget = int.MaxValue; + if (_budget < 0) + _budget = int.MaxValue; return; } @@ -420,9 +437,11 @@ private void GrowTable(Tables tables) checked { newLength = tables._buckets.Length * 2 + 1; - while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) newLength += 2; + while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) + newLength += 2; - if (newLength > MaxArrayLength) maximizeTableSize = true; + if (newLength > MaxArrayLength) + maximizeTableSize = true; } } catch (OverflowException) @@ -444,7 +463,8 @@ private void GrowTable(Tables tables) { newLocks = new object[tables._locks.Length * 2]; Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); - for (int i = tables._locks.Length; i < newLocks.Length; i++) newLocks[i] = new object(); + for (int i = tables._locks.Length; i < newLocks.Length; i++) + newLocks[i] = new object(); } Node[] newBuckets = new Node[newLength]; @@ -497,7 +517,8 @@ private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcqui } finally { - if (lockTaken) locksAcquired++; + if (lockTaken) + locksAcquired++; } } } diff --git a/src/Kook.Net.Core/Utils/MentionUtils.cs b/src/Kook.Net.Core/Utils/MentionUtils.cs index 94a702cd..ebf5b37a 100644 --- a/src/Kook.Net.Core/Utils/MentionUtils.cs +++ b/src/Kook.Net.Core/Utils/MentionUtils.cs @@ -37,7 +37,7 @@ public static class MentionUtils RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); - private const char SanitizeChar = '\x200b'; + private const char SanitizeChar = '\u200b'; internal static string KMarkdownMentionUser(string id) => $"(met){id}(met)"; @@ -100,7 +100,6 @@ public static string PlainTextMentionUser(string username, ulong id) => /// public static string PlainTextMentionRole(uint id) => PlainTextMentionRole(id.ToString()); - /// /// Parses a provided user mention string. /// @@ -109,7 +108,8 @@ public static string PlainTextMentionUser(string username, ulong id) => /// Invalid mention format. public static ulong ParseUser(string text, TagMode tagMode) { - if (TryParseUser(text, out ulong id, tagMode)) return id; + if (TryParseUser(text, out ulong id, tagMode)) + return id; throw new ArgumentException("Invalid mention format.", nameof(text)); } @@ -144,7 +144,8 @@ public static bool TryParseUser(string text, out ulong userId, TagMode tagMode) /// Invalid mention format. public static ulong ParseChannel(string text, TagMode tagMode) { - if (TryParseChannel(text, out ulong id, tagMode)) return id; + if (TryParseChannel(text, out ulong id, tagMode)) + return id; throw new ArgumentException("Invalid mention format.", nameof(text)); } @@ -203,11 +204,7 @@ public static bool TryParseRole(string text, out uint roleId, TagMode tagMode) internal static string Resolve(IMessage msg, int startIndex, TagHandling userHandling, TagHandling channelHandling, TagHandling roleHandling, TagHandling everyoneHandling, TagHandling emojiHandling) { -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER StringBuilder text = new(msg.Content[startIndex..]); -#else - var text = new StringBuilder(msg.Content.Substring(startIndex)); -#endif IReadOnlyCollection tags = msg.Tags; int indexOffset = -startIndex; @@ -215,39 +212,42 @@ internal static string Resolve(IMessage msg, int startIndex, TagHandling userHan { if (tag.Index < startIndex) continue; - string newText = ""; + string newText; switch (tag.Type) { case TagType.UserMention: - if (userHandling == TagHandling.Ignore) continue; - + if (userHandling == TagHandling.Ignore) + continue; newText = ResolveUserMention(tag, userHandling); break; case TagType.ChannelMention: - if (channelHandling == TagHandling.Ignore) continue; - + if (channelHandling == TagHandling.Ignore) + continue; newText = ResolveChannelMention(tag, channelHandling); break; case TagType.RoleMention: - if (roleHandling == TagHandling.Ignore) continue; - + if (roleHandling == TagHandling.Ignore) + continue; newText = ResolveRoleMention(tag, roleHandling); break; case TagType.EveryoneMention: - if (everyoneHandling == TagHandling.Ignore) continue; - + if (everyoneHandling == TagHandling.Ignore) + continue; newText = ResolveEveryoneMention(tag, everyoneHandling); break; case TagType.HereMention: - if (everyoneHandling == TagHandling.Ignore) continue; - + if (everyoneHandling == TagHandling.Ignore) + continue; newText = ResolveHereMention(tag, everyoneHandling); break; case TagType.Emoji: - if (emojiHandling == TagHandling.Ignore) continue; - + if (emojiHandling == TagHandling.Ignore) + continue; newText = ResolveEmoji(tag, emojiHandling); break; + default: + newText = string.Empty; + break; } text.Remove(tag.Index + indexOffset, tag.Length); @@ -260,156 +260,96 @@ internal static string Resolve(IMessage msg, int startIndex, TagHandling userHan internal static string ResolveUserMention(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) - { - IUser user = tag.Value as IUser; - IGuildUser guildUser = user as IGuildUser; - switch (mode) - { - case TagHandling.Name: - if (user != null) - return $"@{guildUser?.Nickname ?? user?.Username}"; - else - return ""; - case TagHandling.NameNoPrefix: - if (user != null) - return $"{guildUser?.Nickname ?? user?.Username}"; - else - return ""; - case TagHandling.FullName: - if (user != null) - return $"@{user.Username}#{user.IdentifyNumber}"; - else - return ""; - case TagHandling.FullNameNoPrefix: - if (user != null) - return $"{user.Username}#{user.IdentifyNumber}"; - else - return ""; - case TagHandling.Sanitize: - return KMarkdownMentionUser($"{SanitizeChar}{tag.Key}"); - } - } + if (mode == TagHandling.Remove) + return string.Empty; + if (tag.Value is not IUser user) + return string.Empty; - return ""; + IGuildUser? guildUser = user as IGuildUser; + return mode switch + { + TagHandling.Name => $"@{guildUser?.Nickname ?? user.Username}", + TagHandling.NameNoPrefix => $"{guildUser?.Nickname ?? user.Username}", + TagHandling.FullName => $"@{user.Username}#{user.IdentifyNumber}", + TagHandling.FullNameNoPrefix => $"{user.Username}#{user.IdentifyNumber}", + TagHandling.Sanitize => KMarkdownMentionUser($"{SanitizeChar}{tag.Key}"), + _ => string.Empty + }; } internal static string ResolveChannelMention(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) + if (mode == TagHandling.Remove) + return string.Empty; + if (tag.Value is not IChannel channel) + return string.Empty; + return mode switch { - IChannel channel = tag.Value as IChannel; - switch (mode) - { - case TagHandling.Name: - case TagHandling.FullName: - if (channel != null) - return $"#{channel.Name}"; - else - return ""; - case TagHandling.NameNoPrefix: - case TagHandling.FullNameNoPrefix: - if (channel != null) - return $"{channel.Name}"; - else - return ""; - case TagHandling.Sanitize: - return KMarkdownMentionChannel($"{SanitizeChar}{tag.Key}"); - } - } - - return ""; + TagHandling.Name or TagHandling.FullName => $"#{channel.Name}", + TagHandling.NameNoPrefix or TagHandling.FullNameNoPrefix => $"{channel.Name}", + TagHandling.Sanitize => KMarkdownMentionChannel($"{SanitizeChar}{tag.Key}"), + _ => string.Empty + }; } internal static string ResolveRoleMention(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) + if (mode == TagHandling.Remove) + return string.Empty; + if (tag.Value is not IRole role) + return string.Empty; + return mode switch { - IRole role = tag.Value as IRole; - switch (mode) - { - case TagHandling.Name: - case TagHandling.FullName: - if (role != null) - return $"@{role.Name}"; - else - return ""; - case TagHandling.NameNoPrefix: - case TagHandling.FullNameNoPrefix: - if (role != null) - return $"{role.Name}"; - else - return ""; - case TagHandling.Sanitize: - return KMarkdownMentionRole($"{SanitizeChar}{tag.Key}"); - } - } - - return ""; + TagHandling.Name or TagHandling.FullName => $"@{role.Name}", + TagHandling.NameNoPrefix or TagHandling.FullNameNoPrefix => $"{role.Name}", + TagHandling.Sanitize => KMarkdownMentionRole($"{SanitizeChar}{tag.Key}"), + _ => string.Empty + }; } internal static string ResolveEveryoneMention(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) - switch (mode) - { - case TagHandling.Name: - case TagHandling.FullName: - return "@全体成员"; - case TagHandling.NameNoPrefix: - case TagHandling.FullNameNoPrefix: - return "全体成员"; - case TagHandling.Sanitize: - return $"@{SanitizeChar}全体成员"; - } - - return ""; + if (mode == TagHandling.Remove) + return string.Empty; + return mode switch + { + TagHandling.Name or TagHandling.FullName => "@全体成员", + TagHandling.NameNoPrefix or TagHandling.FullNameNoPrefix => "全体成员", + TagHandling.Sanitize => $"@{SanitizeChar}全体成员", + _ => string.Empty + }; } internal static string ResolveHereMention(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) - switch (mode) - { - case TagHandling.Name: - case TagHandling.FullName: - return "@在线成员"; - case TagHandling.NameNoPrefix: - case TagHandling.FullNameNoPrefix: - return "在线成员"; - case TagHandling.Sanitize: - return $"@{SanitizeChar}在线成员"; - } - - return ""; + if (mode == TagHandling.Remove) + return string.Empty; + return mode switch + { + TagHandling.Name or TagHandling.FullName => "@在线成员", + TagHandling.NameNoPrefix or TagHandling.FullNameNoPrefix => "在线成员", + TagHandling.Sanitize => $"@{SanitizeChar}在线成员", + _ => "" + }; } internal static string ResolveEmoji(ITag tag, TagHandling mode) { - if (mode != TagHandling.Remove) - { - Emote emoji = (Emote)tag.Value; - - //Remove if its name contains any bad chars (prevents a few tag exploits) - for (int i = 0; i < emoji.Name.Length; i++) - { - char c = emoji.Name[i]; - if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') return ""; - } + if (mode == TagHandling.Remove) + return string.Empty; + if (tag.Value is not Emote emoji) + return string.Empty; - switch (mode) - { - case TagHandling.Name: - case TagHandling.FullName: - return $":{emoji.Name}:"; - case TagHandling.NameNoPrefix: - case TagHandling.FullNameNoPrefix: - return $"{emoji.Name}"; - case TagHandling.Sanitize: - return $"[{SanitizeChar}{emoji.Name}{SanitizeChar}:{emoji.Id}]"; - } - } + //Remove if its name contains any bad chars (prevents a few tag exploits) + if (emoji.Name.Any(c => !char.IsLetterOrDigit(c) && c != '_' && c != '-')) + return string.Empty; - return ""; + return mode switch + { + TagHandling.Name or TagHandling.FullName => $":{emoji.Name}:", + TagHandling.NameNoPrefix or TagHandling.FullNameNoPrefix => $"{emoji.Name}", + TagHandling.Sanitize => $"[{SanitizeChar}{emoji.Name}{SanitizeChar}:{emoji.Id}]", + _ => "" + }; } } diff --git a/src/Kook.Net.Core/Utils/Paging/PagedEnumerator.cs b/src/Kook.Net.Core/Utils/Paging/PagedEnumerator.cs index ba810d87..867eaed4 100644 --- a/src/Kook.Net.Core/Utils/Paging/PagedEnumerator.cs +++ b/src/Kook.Net.Core/Utils/Paging/PagedEnumerator.cs @@ -10,7 +10,7 @@ internal class PagedAsyncEnumerable : IAsyncEnumerable private readonly Func, bool> _nextPage; public PagedAsyncEnumerable(int pageSize, Func>> getPage, - Func, bool> nextPage = null, + Func, bool>? nextPage, Guid? start = null, int? count = null) { PageSize = pageSize; @@ -18,7 +18,7 @@ public PagedAsyncEnumerable(int pageSize, Func false); } public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = new()) => @@ -34,6 +34,7 @@ internal class Enumerator : IAsyncEnumerator> public Enumerator(PagedAsyncEnumerable source, CancellationToken token) { + Current = []; _source = source; _token = token; _info = new PageInfo(source._start, source._count, source.PageSize); @@ -61,17 +62,16 @@ public async ValueTask MoveNextAsync() _info.PageSize = _info.Remaining != null ? Math.Min(_info.Remaining.Value, _source.PageSize) : _source.PageSize; - if (_info.Remaining != 0) - if (!_source._nextPage(_info, data)) - _info.Remaining = 0; + if (_info.Remaining != 0 && !_source._nextPage(_info, data)) + _info.Remaining = 0; return true; } public ValueTask DisposeAsync() { - Current = null; - return default(ValueTask); + Current = []; + return default; } } } diff --git a/src/Kook.Net.Core/Utils/Permissions.cs b/src/Kook.Net.Core/Utils/Permissions.cs index ed98b44e..5fa3eaba 100644 --- a/src/Kook.Net.Core/Utils/Permissions.cs +++ b/src/Kook.Net.Core/Utils/Permissions.cs @@ -7,42 +7,41 @@ internal static class Permissions public const int MaxBits = 29; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission flag) - => GetValue(allow, deny, (ulong)flag); + public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission flag) => + GetValue(allow, deny, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, GuildPermission flag) - => GetValue(allow, deny, (ulong)flag); + public static PermValue GetValue(ulong allow, ulong deny, GuildPermission flag) => + GetValue(allow, deny, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static PermValue GetValue(ulong allow, ulong deny, ulong flag) { if (HasFlag(allow, flag)) return PermValue.Allow; - else if (HasFlag(deny, flag)) + if (HasFlag(deny, flag)) return PermValue.Deny; - else - return PermValue.Inherit; + return PermValue.Inherit; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, ChannelPermission flag) - => GetValue(value, (ulong)flag); + public static bool GetValue(ulong value, ChannelPermission flag) => + GetValue(value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, GuildPermission flag) - => GetValue(value, (ulong)flag); + public static bool GetValue(ulong value, GuildPermission flag) => + GetValue(value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool GetValue(ulong value, ulong flag) => HasFlag(value, flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission flag) - => SetValue(ref rawValue, value, (ulong)flag); + public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission flag) => + SetValue(ref rawValue, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, GuildPermission flag) - => SetValue(ref rawValue, value, (ulong)flag); + public static void SetValue(ref ulong rawValue, bool? value, GuildPermission flag) => + SetValue(ref rawValue, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetValue(ref ulong rawValue, bool? value, ulong flag) @@ -57,12 +56,12 @@ public static void SetValue(ref ulong rawValue, bool? value, ulong flag) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission flag) - => SetValue(ref allow, ref deny, value, (ulong)flag); + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission flag) => + SetValue(ref allow, ref deny, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission flag) - => SetValue(ref allow, ref deny, value, (ulong)flag); + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission flag) => + SetValue(ref allow, ref deny, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ulong flag) @@ -121,56 +120,67 @@ public static ulong ResolveGuild(IGuild guild, IGuildUser user) }*/ public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel channel, ulong guildPermissions) { - ulong resolvedPermissions = 0; + ulong resolvedPermissions; ulong mask = ChannelPermissions.All(channel).RawValue; if (GetValue(guildPermissions, GuildPermission.Administrator)) //Includes owner - resolvedPermissions = mask; //Owners and administrators always have all permissions + resolvedPermissions = mask; //Owners and administrators always have all permissions else { //Start with this user's guild permissions resolvedPermissions = guildPermissions; //Give/Take Everyone permissions - OverwritePermissions? perms = channel.GetPermissionOverwrite(guild.EveryoneRole); - if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + if (channel.GetPermissionOverwrite(guild.EveryoneRole) is {} everyoneRolePerms) + { + resolvedPermissions &= ~everyoneRolePerms.DenyValue; + resolvedPermissions |= everyoneRolePerms.AllowValue; + } //Give/Take Role permissions - ulong deniedPermissions = 0UL, allowedPermissions = 0UL; + ulong deniedPermissions = 0UL; + ulong allowedPermissions = 0UL; foreach (uint roleId in user.RoleIds) { - IRole role; - if (roleId != guild.EveryoneRole.Id && (role = guild.GetRole(roleId)) != null) - { - perms = channel.GetPermissionOverwrite(role); - if (perms != null) - { - allowedPermissions |= perms.Value.AllowValue; - deniedPermissions |= perms.Value.DenyValue; - } - } + if (roleId == guild.EveryoneRole.Id + || guild.GetRole(roleId) is not { } role + || channel.GetPermissionOverwrite(role) is not { } rolePerms) + continue; + allowedPermissions |= rolePerms.AllowValue; + deniedPermissions |= rolePerms.DenyValue; } - resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; + resolvedPermissions &= ~deniedPermissions; + resolvedPermissions |= allowedPermissions; //Give/Take User permissions - perms = channel.GetPermissionOverwrite(user); - if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + if (channel.GetPermissionOverwrite(user) is { } userPerms) + { + resolvedPermissions &= ~userPerms.DenyValue; + resolvedPermissions |= userPerms.AllowValue; + } if (channel is ITextChannel) { if (!GetValue(resolvedPermissions, ChannelPermission.ViewChannel)) //No read permission on a text channel removes all other permissions resolvedPermissions = 0; - else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) + else { //No send permissions on a text channel removes all send-related permissions - resolvedPermissions &= ~(ulong)ChannelPermission.MentionEveryone; - resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; + if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) + { + resolvedPermissions &= ~(ulong)ChannelPermission.MentionEveryone; + resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; + } + // Connect permission overrides passive voice connect permissions + if (GetValue(resolvedPermissions, ChannelPermission.Connect)) + resolvedPermissions |= (ulong)ChannelPermission.PassiveConnect; } } - resolvedPermissions &= mask; //Ensure we didn't get any permissions this channel doesn't support (from guildPerms, for example) + //Ensure we didn't get any permissions this channel doesn't support (from guildPerms, for example) + resolvedPermissions &= mask; } return resolvedPermissions; diff --git a/src/Kook.Net.Core/Utils/Preconditions.cs b/src/Kook.Net.Core/Utils/Preconditions.cs index 6dcf9b80..cfb6f5b7 100644 --- a/src/Kook.Net.Core/Utils/Preconditions.cs +++ b/src/Kook.Net.Core/Utils/Preconditions.cs @@ -1,232 +1,241 @@ +using System.Diagnostics.CodeAnalysis; + namespace Kook; +#nullable enable + /// /// Provides methods to check preconditions. /// public static class Preconditions { #region Objects - /// must not be null. - public static void NotNull(T obj, string name, string msg = null) where T : class { if (obj == null) throw CreateNotNullException(name, msg); } - private static ArgumentNullException CreateNotNullException(string name, string msg) + /// must not be null. + public static void NotNull([NotNull] T? obj, string name, string? msg = null) where T : class { - if (msg == null) - return new ArgumentNullException(paramName: name); - else - return new ArgumentNullException(paramName: name, message: msg); + if (obj == null) + throw CreateNotNullException(name, msg); } + + private static ArgumentNullException CreateNotNullException(string name, string? msg) => + new(paramName: name, message: msg); + #endregion #region Strings + /// cannot be blank. - public static void NotEmpty(string obj, string name, string msg = null) { if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } + public static void NotEmpty(string obj, string name, string? msg = null) + { + if (obj.Length == 0) + throw CreateNotEmptyException(name, msg); + } + /// cannot be blank. /// must not be null. - public static void NotNullOrEmpty(string obj, string name, string msg = null) + public static void NotNullOrEmpty([NotNull] string? obj, string name, string? msg = null) { - if (obj == null) + if (obj == null || obj.Length == 0) throw CreateNotNullException(name, msg); - if (obj.Length == 0) - throw CreateNotEmptyException(name, msg); } /// cannot be blank. /// must not be null. - public static void NotNullOrWhitespace(string obj, string name, string msg = null) + public static void NotNullOrWhitespace([NotNull] string? obj, string name, string? msg = null) { - if (obj == null) + if (obj == null || obj.Trim().Length == 0) throw CreateNotNullException(name, msg); - if (obj.Trim().Length == 0) - throw CreateNotEmptyException(name, msg); } - private static ArgumentException CreateNotEmptyException(string name, string msg) - => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); + private static ArgumentException CreateNotEmptyException(string name, string? msg) => + new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); + #endregion #region Numerics /// Value may not be equal to . - public static void NotEqual(sbyte obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(sbyte obj, sbyte value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(byte obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(byte obj, byte value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(short obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(short obj, short value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(ushort obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ushort obj, ushort value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(int obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(int obj, int value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(uint obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(uint obj, uint value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(long obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(long obj, long value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(ulong obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ulong obj, ulong value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(Guid obj, Guid value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Guid obj, Guid value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(sbyte? obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(sbyte? obj, sbyte value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(byte? obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(byte? obj, byte value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(short? obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(short? obj, short value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(ushort? obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ushort? obj, ushort value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(int? obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(int? obj, int value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(uint? obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(uint? obj, uint value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(long? obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(long? obj, long value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(ulong? obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ulong? obj, ulong value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } /// Value may not be equal to . - public static void NotEqual(Guid? obj, Guid value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Guid? obj, Guid value, string name, string? msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } - private static ArgumentException CreateNotEqualException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); + private static ArgumentException CreateNotEqualException(string name, string? msg, T value) => + new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); /// Value must be at least . - public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(sbyte obj, sbyte value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(byte obj, byte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(byte obj, byte value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(short obj, short value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(short obj, short value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(ushort obj, ushort value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ushort obj, ushort value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(int obj, int value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(int obj, int value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(uint obj, uint value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(uint obj, uint value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(long obj, long value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(long obj, long value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(ulong obj, ulong value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ulong obj, ulong value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(sbyte? obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(sbyte? obj, sbyte value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(byte? obj, byte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(byte? obj, byte value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(short? obj, short value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(short? obj, short value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(ushort? obj, ushort value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ushort? obj, ushort value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(int? obj, int value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(int? obj, int value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(uint? obj, uint value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(uint? obj, uint value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(long? obj, long value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(long? obj, long value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . - public static void AtLeast(ulong? obj, ulong value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ulong? obj, ulong value, string name, string? msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } - private static ArgumentException CreateAtLeastException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); + private static ArgumentException CreateAtLeastException(string name, string? msg, T value) => + new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); /// Value must be greater than . - public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(sbyte obj, sbyte value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(byte obj, byte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(byte obj, byte value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(short obj, short value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(short obj, short value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(ushort obj, ushort value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ushort obj, ushort value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(int obj, int value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(int obj, int value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(uint obj, uint value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(uint obj, uint value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(long obj, long value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(long obj, long value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(ulong obj, ulong value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ulong obj, ulong value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(sbyte? obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(sbyte? obj, sbyte value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(byte? obj, byte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(byte? obj, byte value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(short? obj, short value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(short? obj, short value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(ushort? obj, ushort value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ushort? obj, ushort value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(int? obj, int value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(int? obj, int value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(uint? obj, uint value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(uint? obj, uint value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(long? obj, long value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(long? obj, long value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . - public static void GreaterThan(ulong? obj, ulong value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ulong? obj, ulong value, string name, string? msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } - private static ArgumentException CreateGreaterThanException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); + private static ArgumentException CreateGreaterThanException(string name, string? msg, T value) => + new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); /// Value must be at most . - public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(sbyte obj, sbyte value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(byte obj, byte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(byte obj, byte value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(short obj, short value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(short obj, short value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(ushort obj, ushort value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ushort obj, ushort value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(int obj, int value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(int obj, int value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(uint obj, uint value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(uint obj, uint value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(long obj, long value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(long obj, long value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(ulong obj, ulong value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ulong obj, ulong value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(sbyte? obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(sbyte? obj, sbyte value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(byte? obj, byte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(byte? obj, byte value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(short? obj, short value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(short? obj, short value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(ushort? obj, ushort value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ushort? obj, ushort value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(int? obj, int value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(int? obj, int value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(uint? obj, uint value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(uint? obj, uint value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(long? obj, long value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(long? obj, long value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . - public static void AtMost(ulong? obj, ulong value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ulong? obj, ulong value, string name, string? msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } - private static ArgumentException CreateAtMostException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); + private static ArgumentException CreateAtMostException(string name, string? msg, T value) => + new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); /// Value must be less than . - public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(sbyte obj, sbyte value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(byte obj, byte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(byte obj, byte value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(short obj, short value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(short obj, short value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(ushort obj, ushort value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ushort obj, ushort value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(int obj, int value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(int obj, int value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(uint obj, uint value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(uint obj, uint value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(long obj, long value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(long obj, long value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(ulong obj, ulong value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ulong obj, ulong value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(sbyte? obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(sbyte? obj, sbyte value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(byte? obj, byte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(byte? obj, byte value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(short? obj, short value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(short? obj, short value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(ushort? obj, ushort value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ushort? obj, ushort value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(int? obj, int value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(int? obj, int value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(uint? obj, uint value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(uint? obj, uint value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(long? obj, long value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(long? obj, long value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . - public static void LessThan(ulong? obj, ulong value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ulong? obj, ulong value, string name, string? msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + + private static ArgumentException CreateLessThanException(string name, string? msg, T value) => + new ArgumentException(message: msg ?? $"Value must be less than {value}.", paramName: name); - private static ArgumentException CreateLessThanException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be less than {value}.", paramName: name); #endregion } diff --git a/src/Kook.Net.Core/Utils/RoleUtils.cs b/src/Kook.Net.Core/Utils/RoleUtils.cs index f2720919..91dab5e4 100644 --- a/src/Kook.Net.Core/Utils/RoleUtils.cs +++ b/src/Kook.Net.Core/Utils/RoleUtils.cs @@ -2,16 +2,13 @@ namespace Kook; internal static class RoleUtils { - public static int Compare(IRole left, IRole right) + public static int Compare(IRole? left, IRole? right) { if (left == null) return -1; - if (right == null) return 1; - int result = left.Position.CompareTo(right.Position); // As per Kook's documentation, a tie is broken by ID if (result != 0) return result; - return left.Id.CompareTo(right.Id); } } diff --git a/src/Kook.Net.Core/Utils/TokenUtils.cs b/src/Kook.Net.Core/Utils/TokenUtils.cs index 0c9548c2..da1b8569 100644 --- a/src/Kook.Net.Core/Utils/TokenUtils.cs +++ b/src/Kook.Net.Core/Utils/TokenUtils.cs @@ -26,6 +26,9 @@ public static class TokenUtils /// internal const int StandardBotTokenLength = 35; + /// + /// The padding character used in base64 encoding. + /// internal const char Base64Padding = '='; /// @@ -54,15 +57,16 @@ internal static string PadBase64String(string encodedBase64) "The supplied base64-encoded string was null or whitespace."); // do not pad if already contains padding characters - if (encodedBase64.IndexOf(Base64Padding) != -1) return encodedBase64; + if (encodedBase64.IndexOf(Base64Padding) != -1) + return encodedBase64; // based from https://stackoverflow.com/a/1228744 int padding = (4 - encodedBase64.Length % 4) % 4; if (padding == 3) // can never have 3 characters of padding throw new FormatException("The provided base64 string is corrupt, as it requires an invalid amount of padding."); - else if (padding == 0) return encodedBase64; - + if (padding == 0) + return encodedBase64; return encodedBase64.PadRight(encodedBase64.Length + padding, Base64Padding); } @@ -128,7 +132,7 @@ internal static bool CheckBotTokenValidity(string message) /// /// The set of all characters that are not allowed inside of a token. /// - internal static readonly char[] IllegalTokenCharacters = { ' ', '\t', '\r', '\n' }; + internal static readonly char[] IllegalTokenCharacters = [' ', '\t', '\r', '\n']; /// /// Checks if the given token contains a whitespace or newline character @@ -138,8 +142,8 @@ internal static bool CheckBotTokenValidity(string message) /// /// true if the token contains a whitespace or newline character. /// - internal static bool CheckContainsIllegalCharacters(string token) - => token.IndexOfAny(IllegalTokenCharacters) != -1; + internal static bool CheckContainsIllegalCharacters(string token) => + token.IndexOfAny(IllegalTokenCharacters) != -1; /// /// Checks the validity of the supplied token of a specific type. diff --git a/src/Kook.Net.Core/Utils/UrlValidation.cs b/src/Kook.Net.Core/Utils/UrlValidation.cs index 9f5d1278..cfa585ba 100644 --- a/src/Kook.Net.Core/Utils/UrlValidation.cs +++ b/src/Kook.Net.Core/Utils/UrlValidation.cs @@ -8,17 +8,17 @@ internal static class UrlValidation /// Not full URL validation right now. Just ensures protocol is present and that it's either http or https. /// /// The URL to validate before sending to Kook. - /// A URL must include a protocol (http or https). + /// A URL cannot be null or empty. + /// A URL must include a protocol (http or https). /// true if URL is valid by our standard, false if null, throws an error upon invalid. - public static bool Validate(string url) + public static void Validate(string url) { - if (string.IsNullOrEmpty(url)) return false; + if (string.IsNullOrEmpty(url)) + throw new UriFormatException("The URL cannot be null or empty."); if (!(url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) - throw new InvalidOperationException($"The url {url} must include a protocol (either HTTP or HTTPS)"); - - return true; + throw new UriFormatException($"The url {url} must include a protocol (either HTTP or HTTPS)"); } /// diff --git a/src/Kook.Net.Core/Utils/ValueHelper.cs b/src/Kook.Net.Core/Utils/ValueHelper.cs new file mode 100644 index 00000000..a8ac32c1 --- /dev/null +++ b/src/Kook.Net.Core/Utils/ValueHelper.cs @@ -0,0 +1,14 @@ +namespace Kook; + +internal static class ValueHelper +{ + public static bool SetIfChanged(Func getter, Action setter, T value, Func? comparer = null) + { + T oldValue = getter(); + bool equals = comparer?.Invoke(oldValue, value) + ?? EqualityComparer.Default.Equals(oldValue, value); + if (equals) return false; + setter(value); + return true; + } +} diff --git a/src/Kook.Net.Experimental/Core/Entities/Guilds/GuildProperties.cs b/src/Kook.Net.Experimental/Core/Entities/Guilds/GuildProperties.cs index 0bf7c3b4..eb5c5b86 100644 --- a/src/Kook.Net.Experimental/Core/Entities/Guilds/GuildProperties.cs +++ b/src/Kook.Net.Experimental/Core/Entities/Guilds/GuildProperties.cs @@ -11,20 +11,15 @@ namespace Kook; /// public class GuildProperties { - /// - /// Gets or sets the identifier of the guild to modify. - /// - public ulong GuildId { get; set; } - /// /// Gets or sets the region for the guild's voice connections. /// - public IVoiceRegion Region { get; set; } + public IVoiceRegion? Region { get; set; } /// /// Gets or sets the ID of the region for the guild's voice connections. /// - public string RegionId { get; set; } + public string? RegionId { get; set; } /// /// Gets or sets the ID of the default channel. @@ -42,7 +37,7 @@ public class GuildProperties /// An which is the default channel; null if nothing changes. /// To clear the manually assigned default channel, set to 0 instead. /// - public ITextChannel DefaultChannel { get; set; } + public ITextChannel? DefaultChannel { get; set; } /// /// Gets or sets the ID of welcome channel. @@ -60,7 +55,7 @@ public class GuildProperties /// An where welcome messages are sent; null if nothing changes. /// To clear the welcome channel, set to 0 instead. /// - public ITextChannel WelcomeChannel { get; set; } + public ITextChannel? WelcomeChannel { get; set; } /// /// Gets or sets whether the guild is open. @@ -85,5 +80,5 @@ public class GuildProperties /// null if nothing changes; To clear the widget channel, /// set to 0 instead. /// - public ITextChannel WidgetChannel { get; set; } + public ITextChannel? WidgetChannel { get; set; } } diff --git a/src/Kook.Net.Experimental/Rest/API/Common/VoiceRegion.cs b/src/Kook.Net.Experimental/Rest/API/Common/VoiceRegion.cs index af72a22e..4395a6fa 100644 --- a/src/Kook.Net.Experimental/Rest/API/Common/VoiceRegion.cs +++ b/src/Kook.Net.Experimental/Rest/API/Common/VoiceRegion.cs @@ -5,10 +5,10 @@ namespace Kook.API; internal class VoiceRegion { [JsonPropertyName("id")] - public string Id { get; set; } + public required string Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("crowding")] public int Crowding { get; set; } diff --git a/src/Kook.Net.Experimental/Rest/API/Rest/CreateGuildParams.cs b/src/Kook.Net.Experimental/Rest/API/Rest/CreateGuildParams.cs index 2c7ea929..70e41755 100644 --- a/src/Kook.Net.Experimental/Rest/API/Rest/CreateGuildParams.cs +++ b/src/Kook.Net.Experimental/Rest/API/Rest/CreateGuildParams.cs @@ -6,7 +6,7 @@ namespace Kook.API.Rest; internal class CreateGuildParams { [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("icon")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] @@ -15,7 +15,7 @@ internal class CreateGuildParams [JsonPropertyName("region")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string RegionId { get; set; } + public string? RegionId { get; set; } [JsonPropertyName("template_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/Kook.Net.Experimental/Rest/API/Rest/DeleteGuildParams.cs b/src/Kook.Net.Experimental/Rest/API/Rest/DeleteGuildParams.cs index 3ea5cdf7..c5b8c7b3 100644 --- a/src/Kook.Net.Experimental/Rest/API/Rest/DeleteGuildParams.cs +++ b/src/Kook.Net.Experimental/Rest/API/Rest/DeleteGuildParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteGuildParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } public static implicit operator DeleteGuildParams(ulong guildId) => new() { GuildId = guildId }; } diff --git a/src/Kook.Net.Experimental/Rest/API/Rest/DisconnectUserParams.cs b/src/Kook.Net.Experimental/Rest/API/Rest/DisconnectUserParams.cs index 9854e0a1..d1213211 100644 --- a/src/Kook.Net.Experimental/Rest/API/Rest/DisconnectUserParams.cs +++ b/src/Kook.Net.Experimental/Rest/API/Rest/DisconnectUserParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class DisconnectUserParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Experimental/Rest/API/Rest/ModifyGuildParams.cs b/src/Kook.Net.Experimental/Rest/API/Rest/ModifyGuildParams.cs index 983deee0..273d9be5 100644 --- a/src/Kook.Net.Experimental/Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Kook.Net.Experimental/Rest/API/Rest/ModifyGuildParams.cs @@ -8,7 +8,7 @@ internal class ModifyGuildParams public ulong GuildId { get; set; } [JsonPropertyName("region")] - public string RegionId { get; set; } + public string? RegionId { get; set; } [JsonPropertyName("default_channel_id")] public ulong? DefaultChannelId { get; set; } diff --git a/src/Kook.Net.Experimental/Rest/API/Rest/ValidateCardsParams.cs b/src/Kook.Net.Experimental/Rest/API/Rest/ValidateCardsParams.cs index 9f8be263..7e4b243d 100644 --- a/src/Kook.Net.Experimental/Rest/API/Rest/ValidateCardsParams.cs +++ b/src/Kook.Net.Experimental/Rest/API/Rest/ValidateCardsParams.cs @@ -6,7 +6,7 @@ namespace Kook.API.Rest; internal class ValidateCardsParams { [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } public static implicit operator ValidateCardsParams(string content) => new() {Content = content}; diff --git a/src/Kook.Net.Experimental/Rest/Entities/Channels/ExperimentalChannelHelper.cs b/src/Kook.Net.Experimental/Rest/Entities/Channels/ExperimentalChannelHelper.cs index 9fe209c2..212de5a4 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Channels/ExperimentalChannelHelper.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Channels/ExperimentalChannelHelper.cs @@ -8,13 +8,16 @@ internal static class ExperimentalChannelHelper #region Permissions /// This channel does not have a parent channel. - public static async Task SyncPermissionsAsync(INestedChannel channel, BaseKookClient client, - RequestOptions options) + public static async Task SyncPermissionsAsync(INestedChannel channel, BaseKookClient client, RequestOptions? options) { - ICategoryChannel category = await ChannelHelper.GetCategoryAsync(channel, client, options).ConfigureAwait(false); - if (category == null) throw new InvalidOperationException("This channel does not have a parent channel."); - - SyncChannelPermissionsParams args = new(channel.Id); + ICategoryChannel? category = await ChannelHelper.GetCategoryAsync(channel, client, options).ConfigureAwait(false); + if (category == null) + throw new InvalidOperationException("This channel does not have a parent channel."); + + SyncChannelPermissionsParams args = new() + { + ChannelId = channel.Id + }; await client.ApiClient.SyncChannelPermissionsAsync(args, options).ConfigureAwait(false); } @@ -22,7 +25,7 @@ public static async Task SyncPermissionsAsync(INestedChannel channel, BaseKookCl #region Voice - public static async Task DisconnectUserAsync(IVoiceChannel channel, BaseKookClient client, IGuildUser user, RequestOptions options) + public static async Task DisconnectUserAsync(IVoiceChannel channel, BaseKookClient client, IGuildUser user, RequestOptions? options) { DisconnectUserParams args = new() { UserId = user.Id, ChannelId = channel.Id }; await client.ApiClient.DisconnectUserAsync(args, options).ConfigureAwait(false); diff --git a/src/Kook.Net.Experimental/Rest/Entities/Channels/RestTextChannelExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/Entities/Channels/RestTextChannelExperimentalExtensions.cs index cd4abaa3..79d91130 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Channels/RestTextChannelExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Channels/RestTextChannelExperimentalExtensions.cs @@ -19,6 +19,6 @@ public static class RestTextChannelExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task SyncPermissionsAsync(this RestTextChannel channel, RequestOptions options = null) - => ExperimentalChannelHelper.SyncPermissionsAsync(channel, channel.Kook, options); + public static Task SyncPermissionsAsync(this RestTextChannel channel, RequestOptions? options = null) => + ExperimentalChannelHelper.SyncPermissionsAsync(channel, channel.Kook, options); } diff --git a/src/Kook.Net.Experimental/Rest/Entities/Channels/RestVoiceChannelExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/Entities/Channels/RestVoiceChannelExperimentalExtensions.cs index 7c76f374..48397db9 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Channels/RestVoiceChannelExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Channels/RestVoiceChannelExperimentalExtensions.cs @@ -19,8 +19,8 @@ public static class RestVoiceChannelExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task SyncPermissionsAsync(this RestVoiceChannel channel, RequestOptions options = null) - => ExperimentalChannelHelper.SyncPermissionsAsync(channel, channel.Kook, options); + public static Task SyncPermissionsAsync(this RestVoiceChannel channel, RequestOptions? options = null) => + ExperimentalChannelHelper.SyncPermissionsAsync(channel, channel.Kook, options); /// /// Disconnects the specified user from the voice channel. @@ -37,6 +37,6 @@ public static Task SyncPermissionsAsync(this RestVoiceChannel channel, RequestOp /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task DisconnectUserAsync(this RestVoiceChannel channel, IGuildUser user, RequestOptions options = null) - => ExperimentalChannelHelper.DisconnectUserAsync(channel, channel.Kook, user, options); + public static Task DisconnectUserAsync(this RestVoiceChannel channel, IGuildUser user, RequestOptions? options = null) => + ExperimentalChannelHelper.DisconnectUserAsync(channel, channel.Kook, user, options); } diff --git a/src/Kook.Net.Experimental/Rest/Entities/Guilds/ExperimentalGuildHelper.cs b/src/Kook.Net.Experimental/Rest/Entities/Guilds/ExperimentalGuildHelper.cs index ad2a9e05..0dafebe5 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Guilds/ExperimentalGuildHelper.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Guilds/ExperimentalGuildHelper.cs @@ -8,35 +8,42 @@ internal static class ExperimentalGuildHelper { /// is null. public static async Task ModifyAsync(IGuild guild, BaseKookClient client, - Action func, RequestOptions options) + Action func, RequestOptions? options) { if (func == null) throw new ArgumentNullException(nameof(func)); GuildProperties args = new(); func(args); - ModifyGuildParams apiArgs = new() { GuildId = args.GuildId, EnableOpen = args.EnableOpen }; + ModifyGuildParams apiArgs = new() + { + GuildId = guild.Id, + EnableOpen = args.EnableOpen + }; if (args.Region is not null) apiArgs.RegionId = args.Region.Id; - else if (!string.IsNullOrWhiteSpace(args.RegionId)) apiArgs.RegionId = args.RegionId; + else if (!string.IsNullOrWhiteSpace(args.RegionId)) + apiArgs.RegionId = args.RegionId; if (args.DefaultChannel is not null) apiArgs.DefaultChannelId = args.DefaultChannel.Id; - else if (args.DefaultChannelId is not null) apiArgs.DefaultChannelId = args.DefaultChannelId.Value; + else if (args.DefaultChannelId is not null) + apiArgs.DefaultChannelId = args.DefaultChannelId.Value; if (args.WelcomeChannel is not null) apiArgs.WelcomeChannelId = args.WelcomeChannel.Id; - else if (args.WelcomeChannelId is not null) apiArgs.WelcomeChannelId = args.WelcomeChannelId.Value; + else if (args.WelcomeChannelId is not null) + apiArgs.WelcomeChannelId = args.WelcomeChannelId.Value; if (args.WidgetChannel is not null) apiArgs.WidgetChannelId = args.WidgetChannel.Id; - else if (args.WidgetChannelId is not null) apiArgs.WidgetChannelId = args.WidgetChannelId.Value; + else if (args.WidgetChannelId is not null) + apiArgs.WidgetChannelId = args.WidgetChannelId.Value; return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task DeleteAsync(IGuild guild, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteAsync(IGuild guild, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteGuildAsync(guild.Id, options).ConfigureAwait(false); } diff --git a/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestGuildExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestGuildExperimentalExtensions.cs index d842074e..3238eb42 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestGuildExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestGuildExperimentalExtensions.cs @@ -19,8 +19,8 @@ public static class RestGuildExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task DeleteAsync(this RestGuild guild, RequestOptions options = null) - => ExperimentalGuildHelper.DeleteAsync(guild, guild.Kook, options); + public static Task DeleteAsync(this RestGuild guild, RequestOptions? options = null) => + ExperimentalGuildHelper.DeleteAsync(guild, guild.Kook, options); /// /// Modifies this guild. @@ -32,7 +32,7 @@ public static Task DeleteAsync(this RestGuild guild, RequestOptions options = nu /// A task that represents the asynchronous modification operation. /// /// is null. - public static async Task ModifyAsync(this RestGuild guild, Action func, RequestOptions options = null) + public static async Task ModifyAsync(this RestGuild guild, Action func, RequestOptions? options = null) { RichGuild model = await ExperimentalGuildHelper.ModifyAsync(guild, guild.Kook, func, options).ConfigureAwait(false); guild.Update(model); diff --git a/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestVoiceRegion.cs index b1f9ac2d..54158c30 100644 --- a/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Kook.Net.Experimental/Rest/Entities/Guilds/RestVoiceRegion.cs @@ -27,6 +27,7 @@ public class RestVoiceRegion : RestEntity, IVoiceRegion internal RestVoiceRegion(BaseKookClient kook, string id) : base(kook, id) { + Name = string.Empty; } internal static RestVoiceRegion Create(BaseKookClient kook, Model model) diff --git a/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs b/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs index 5025507b..afcd5149 100644 --- a/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs +++ b/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs @@ -10,21 +10,29 @@ internal static class ExperimentalClientHelper #region Guild public static async Task CreateGuildAsync(BaseKookClient client, - string name, IVoiceRegion region = null, Stream icon = null, int? templateId = null, RequestOptions options = null) + string name, IVoiceRegion? region = null, Stream? icon = null, int? templateId = null, RequestOptions? options = null) { - CreateGuildParams args = new() { Name = name, RegionId = region?.Id, TemplateId = templateId }; - if (icon != null) args.Icon = new Image(icon); - + CreateGuildParams args = new() + { + Name = name, + RegionId = region?.Id, + TemplateId = templateId + }; + if (icon != null) + args.Icon = new Image(icon); RichGuild model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } - public static async Task> GetAdminGuildsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetAdminGuildsAsync(BaseKookClient client, RequestOptions? options) { ImmutableArray.Builder guilds = ImmutableArray.CreateBuilder(); - IEnumerable models = await client.ApiClient.GetAdminGuildsAsync(options: options).FlattenAsync().ConfigureAwait(false); - foreach (Guild model in models) guilds.Add(RestGuild.Create(client, model)); - + IEnumerable models = await client.ApiClient + .GetAdminGuildsAsync(options: options) + .FlattenAsync() + .ConfigureAwait(false); + foreach (Guild model in models) + guilds.Add(RestGuild.Create(client, model)); return guilds.ToImmutable(); } @@ -32,13 +40,13 @@ public static async Task> GetAdminGuildsAsync(Bas #region Voice Region - public static async Task> GetVoiceRegionsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetVoiceRegionsAsync(BaseKookClient client, RequestOptions? options) { IEnumerable models = await client.ApiClient.GetVoiceRegionsAsync(options: options).FlattenAsync().ConfigureAwait(false); - return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + return [..models.Select(x => RestVoiceRegion.Create(client, x))]; } - public static async Task GetVoiceRegionAsync(BaseKookClient client, string id, RequestOptions options) + public static async Task GetVoiceRegionAsync(BaseKookClient client, string id, RequestOptions? options) { IEnumerable models = await client.ApiClient.GetVoiceRegionsAsync(options: options).FlattenAsync().ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); @@ -48,13 +56,13 @@ public static async Task GetVoiceRegionAsync(BaseKookClient cli #region Messages - public static Task ValidateCardsAsync(KookRestClient client, IEnumerable cards, RequestOptions options) + public static Task ValidateCardsAsync(KookRestClient client, IEnumerable cards, RequestOptions? options) { ValidateCardsParams args = ValidateCardsParams.FromCards(cards); return client.ApiClient.ValidateCardsAsync(args, options); } - public static Task ValidateCardsAsync(KookRestClient client, string cardsJson, RequestOptions options) + public static Task ValidateCardsAsync(KookRestClient client, string cardsJson, RequestOptions? options) { ValidateCardsParams args = cardsJson; return client.ApiClient.ValidateCardsAsync(args, options); diff --git a/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs index 2a0f1f35..b8282132 100644 --- a/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs @@ -13,58 +13,65 @@ internal static class KookRestApiClientExperimentalExtensions #region Guilds public static IAsyncEnumerable> GetAdminGuildsAsync(this KookRestApiClient client, - int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions options = null) + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); - KookRestApiClient.BucketIds ids = new(); return client.SendPagedAsync(HttpMethod.Get, (pageSize, page) => $"guild/list?page_size={pageSize}&page={page}&type=admin", ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public static async Task CreateGuildAsync(this KookRestApiClient client, CreateGuildParams args, RequestOptions options = null) + public static async Task CreateGuildAsync(this KookRestApiClient client, + CreateGuildParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(); - return await client.SendJsonAsync(HttpMethod.Post, () => "guild/create", args, ids, ClientBucketType.SendEdit, options) + return await client.SendJsonAsync(HttpMethod.Post, + () => "guild/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public static async Task DeleteGuildAsync(this KookRestApiClient client, DeleteGuildParams args, RequestOptions options = null) + public static async Task DeleteGuildAsync(this KookRestApiClient client, + DeleteGuildParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(args.GuildId); - await client.SendJsonAsync(HttpMethod.Post, () => $"guild/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await client.SendJsonAsync(HttpMethod.Post, + () => $"guild/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public static async Task ModifyGuildAsync(this KookRestApiClient client, ulong guildId, ModifyGuildParams args, - RequestOptions options = null) + public static async Task ModifyGuildAsync(this KookRestApiClient client, + ulong guildId, ModifyGuildParams args, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(guildId); - return await client.SendJsonAsync(HttpMethod.Post, () => $"guild/update", args, ids, ClientBucketType.SendEdit, options) + return await client.SendJsonAsync(HttpMethod.Post, + () => $"guild/update", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public static async Task SyncChannelPermissionsAsync(this KookRestApiClient client, SyncChannelPermissionsParams args, - RequestOptions options = null) + public static async Task SyncChannelPermissionsAsync(this KookRestApiClient client, + SyncChannelPermissionsParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(channelId: args.ChannelId); - await client.SendJsonAsync(HttpMethod.Post, () => $"channel-role/sync", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await client.SendJsonAsync(HttpMethod.Post, + () => $"channel-role/sync", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion @@ -72,12 +79,13 @@ public static async Task SyncChannelPermissionsAsync(this KookRestApiClient clie #region Voice Regions public static IAsyncEnumerable> GetVoiceRegionsAsync(this KookRestApiClient client, - int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions options = null) + int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(); PageMeta pageMeta = new(fromPage, limit); - return client.SendPagedAsync(HttpMethod.Get, (pageSize, page) => $"guild/regions&page_size={pageSize}&page={page}", + return client.SendPagedAsync(HttpMethod.Get, + (pageSize, page) => $"guild/regions&page_size={pageSize}&page={page}", ids, ClientBucketType.SendEdit, pageMeta, options); } @@ -85,7 +93,8 @@ public static IAsyncEnumerable> GetVoiceRegions #region Voice - public static async Task DisconnectUserAsync(this KookRestApiClient client, DisconnectUserParams args, RequestOptions options = null) + public static async Task DisconnectUserAsync(this KookRestApiClient client, + DisconnectUserParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); @@ -93,21 +102,26 @@ public static async Task DisconnectUserAsync(this KookRestApiClient client, Disc options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(channelId: args.ChannelId); - await client.SendJsonAsync(HttpMethod.Post, () => $"channel/kickout", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await client.SendJsonAsync(HttpMethod.Post, + () => $"channel/kickout", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Messages - public static async Task ValidateCardsAsync(this KookRestApiClient client, ValidateCardsParams args, RequestOptions options = null) + public static async Task ValidateCardsAsync(this KookRestApiClient client, + ValidateCardsParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(); - await client.SendJsonAsync(HttpMethod.Post, () => $"message/check-card", args, ids, ClientBucketType.Unbucketed, options).ConfigureAwait(false); + await client.SendJsonAsync(HttpMethod.Post, + () => $"message/check-card", args, ids, ClientBucketType.Unbucketed, options) + .ConfigureAwait(false); } #endregion diff --git a/src/Kook.Net.Experimental/Rest/KookRestClientExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/KookRestClientExperimentalExtensions.cs index 3e12e40f..2419e981 100644 --- a/src/Kook.Net.Experimental/Rest/KookRestClientExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/KookRestClientExperimentalExtensions.cs @@ -20,8 +20,8 @@ public static class KookRestClientExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task> GetVoiceRegionsAsync(this KookRestClient client, RequestOptions options = null) - => ExperimentalClientHelper.GetVoiceRegionsAsync(client, options); + public static Task> GetVoiceRegionsAsync(this KookRestClient client, RequestOptions? options = null) => + ExperimentalClientHelper.GetVoiceRegionsAsync(client, options); /// /// Gets a voice region. @@ -39,8 +39,8 @@ public static Task> GetVoiceRegionsAsync(th /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task GetVoiceRegionAsync(this KookRestClient client, string id, RequestOptions options = null) - => ExperimentalClientHelper.GetVoiceRegionAsync(client, id, options); + public static Task GetVoiceRegionAsync(this KookRestClient client, string id, RequestOptions? options = null) => + ExperimentalClientHelper.GetVoiceRegionAsync(client, id, options); /// /// Creates a guild for the logged-in user. @@ -61,11 +61,13 @@ public static Task GetVoiceRegionAsync(this KookRestClient clie /// /// A task that represents the asynchronous creation operation. The task result contains the created guild. /// - public static async Task CreateGuildAsync(this KookRestClient client, - string name, IVoiceRegion region = null, Stream icon = null, int? templateId = null, RequestOptions options = null) + public static async Task CreateGuildAsync(this KookRestClient client, string name, + IVoiceRegion? region = null, Stream? icon = null, int? templateId = null, RequestOptions? options = null) { - RestGuild guild = await ExperimentalClientHelper.CreateGuildAsync(client, name, region, icon, templateId, options); - await guild.UpdateAsync(); + RestGuild guild = await ExperimentalClientHelper + .CreateGuildAsync(client, name, region, icon, templateId, options) + .ConfigureAwait(false); + await guild.UpdateAsync().ConfigureAwait(false); return guild; } @@ -79,8 +81,8 @@ public static async Task CreateGuildAsync(this KookRestClient client, /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of guilds where the current user has the permission. /// - public static Task> GetAdminGuildsAsync(this KookRestClient client, RequestOptions options = null) - => ExperimentalClientHelper.GetAdminGuildsAsync(client, options); + public static Task> GetAdminGuildsAsync(this KookRestClient client, RequestOptions? options = null) => + ExperimentalClientHelper.GetAdminGuildsAsync(client, options); /// /// Validates a card. @@ -91,8 +93,8 @@ public static Task> GetAdminGuildsAsync(this Kook /// /// A task that represents the asynchronous validation operation. /// - public static Task ValidateCardAsync(this KookRestClient client, ICard card, RequestOptions options = null) - => ValidateCardsAsync(client, new[] { card }, options); + public static Task ValidateCardAsync(this KookRestClient client, ICard card, RequestOptions? options = null) => + ValidateCardsAsync(client, [card], options); /// /// Validates a collection of cards. @@ -103,8 +105,8 @@ public static Task ValidateCardAsync(this KookRestClient client, ICard card, Req /// /// A task that represents the asynchronous validation operation. /// - public static Task ValidateCardsAsync(this KookRestClient client, IEnumerable cards, RequestOptions options = null) - => ExperimentalClientHelper.ValidateCardsAsync(client, cards, options); + public static Task ValidateCardsAsync(this KookRestClient client, IEnumerable cards, RequestOptions? options = null) => + ExperimentalClientHelper.ValidateCardsAsync(client, cards, options); /// /// Validates a collection of cards. @@ -115,6 +117,6 @@ public static Task ValidateCardsAsync(this KookRestClient client, IEnumerable /// A task that represents the asynchronous validation operation. /// - public static Task ValidateCardsAsync(this KookRestClient client, string cardsJson, RequestOptions options = null) - => ExperimentalClientHelper.ValidateCardsAsync(client, cardsJson, options); + public static Task ValidateCardsAsync(this KookRestClient client, string cardsJson, RequestOptions? options = null) => + ExperimentalClientHelper.ValidateCardsAsync(client, cardsJson, options); } diff --git a/src/Kook.Net.Experimental/Rest/Net/Converters/ImageBase64DataUriConverter.cs b/src/Kook.Net.Experimental/Rest/Net/Converters/ImageBase64DataUriConverter.cs index e07ec9c7..da47732d 100644 --- a/src/Kook.Net.Experimental/Rest/Net/Converters/ImageBase64DataUriConverter.cs +++ b/src/Kook.Net.Experimental/Rest/Net/Converters/ImageBase64DataUriConverter.cs @@ -10,8 +10,9 @@ internal class ImageBase64DataUriConverter : JsonConverter public override Image? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string raw = reader.GetString(); - if (string.IsNullOrWhiteSpace(raw)) return null; + string? raw = reader.GetString(); + if (string.IsNullOrWhiteSpace(raw)) + return null; Match match = Regex.Match(raw, @"^data:image/(\w+?-)?(?\w+?);base64,(?.+)$"); string type = match.Groups["type"].Value; @@ -37,7 +38,7 @@ public override void Write(Utf8JsonWriter writer, Image? value, JsonSerializerOp } string base64 = Convert.ToBase64String(bytes); - string extension = !string.IsNullOrWhiteSpace(value?.FileExtension) + string? extension = !string.IsNullOrWhiteSpace(value.Value.FileExtension) ? value.Value.FileExtension : "png"; writer.WriteStringValue($"data:image/{extension};base64,{base64}"); diff --git a/src/Kook.Net.Experimental/WebSocket/BaseSocketClientExperimentalExtensions.cs b/src/Kook.Net.Experimental/WebSocket/BaseSocketClientExperimentalExtensions.cs index 24c60ad7..69e245ec 100644 --- a/src/Kook.Net.Experimental/WebSocket/BaseSocketClientExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/WebSocket/BaseSocketClientExperimentalExtensions.cs @@ -26,7 +26,7 @@ public static class BaseSocketClientExperimentalExtensions /// /// A task that represents the asynchronous creation operation. The task result contains the created guild. /// - public static Task CreateGuildAsync(this BaseSocketClient client, - string name, IVoiceRegion region = null, Stream icon = null, int? templateId = null, RequestOptions options = null) - => ExperimentalClientHelper.CreateGuildAsync(client, name, region, icon, templateId, options ?? RequestOptions.Default); + public static Task CreateGuildAsync(this BaseSocketClient client, string name, + IVoiceRegion? region = null, Stream? icon = null, int? templateId = null, RequestOptions? options = null) => + ExperimentalClientHelper.CreateGuildAsync(client, name, region, icon, templateId, options); } diff --git a/src/Kook.Net.Experimental/WebSocket/Entities/Channels/SocketVoiceChannelExperimentalExtensions.cs b/src/Kook.Net.Experimental/WebSocket/Entities/Channels/SocketVoiceChannelExperimentalExtensions.cs index 0bb5b3a9..065f2255 100644 --- a/src/Kook.Net.Experimental/WebSocket/Entities/Channels/SocketVoiceChannelExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/WebSocket/Entities/Channels/SocketVoiceChannelExperimentalExtensions.cs @@ -22,6 +22,6 @@ public static class SocketVoiceChannelExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task DisconnectUserAsync(this SocketVoiceChannel channel, IGuildUser user, RequestOptions options = null) - => ExperimentalChannelHelper.DisconnectUserAsync(channel, channel.Kook, user, options); + public static Task DisconnectUserAsync(this SocketVoiceChannel channel, IGuildUser user, RequestOptions? options = null) => + ExperimentalChannelHelper.DisconnectUserAsync(channel, channel.Kook, user, options); } diff --git a/src/Kook.Net.Experimental/WebSocket/Entities/Guilds/SocketGuildExperimentalExtensions.cs b/src/Kook.Net.Experimental/WebSocket/Entities/Guilds/SocketGuildExperimentalExtensions.cs index cd4b8d6f..0c7b0d6a 100644 --- a/src/Kook.Net.Experimental/WebSocket/Entities/Guilds/SocketGuildExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/WebSocket/Entities/Guilds/SocketGuildExperimentalExtensions.cs @@ -19,8 +19,8 @@ public static class SocketGuildExperimentalExtensions /// usage, may violate the developer rules or policies, not guaranteed to be stable, and may be changed or removed in the future. /// /// - public static Task DeleteAsync(this SocketGuild guild, RequestOptions options = null) - => ExperimentalGuildHelper.DeleteAsync(guild, guild.Kook, options); + public static Task DeleteAsync(this SocketGuild guild, RequestOptions? options = null) => + ExperimentalGuildHelper.DeleteAsync(guild, guild.Kook, options); /// /// Modifies this guild. @@ -38,6 +38,6 @@ public static Task DeleteAsync(this SocketGuild guild, RequestOptions options = /// /// /// is null. - public static Task ModifyAsync(this SocketGuild guild, Action func, RequestOptions options = null) - => ExperimentalGuildHelper.ModifyAsync(guild, guild.Kook, func, options); + public static Task ModifyAsync(this SocketGuild guild, Action func, RequestOptions? options = null) => + ExperimentalGuildHelper.ModifyAsync(guild, guild.Kook, func, options); } diff --git a/src/Kook.Net.Rest/API/Common/Attachment.cs b/src/Kook.Net.Rest/API/Common/Attachment.cs index 9a266964..6cdfb1e1 100644 --- a/src/Kook.Net.Rest/API/Common/Attachment.cs +++ b/src/Kook.Net.Rest/API/Common/Attachment.cs @@ -5,16 +5,16 @@ namespace Kook.API; internal class Attachment { [JsonPropertyName("type")] - public string Type { get; set; } + public required string Type { get; set; } [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("file_type")] - public string FileType { get; set; } + public string? FileType { get; set; } [JsonPropertyName("size")] public int? Size { get; set; } diff --git a/src/Kook.Net.Rest/API/Common/Ban.cs b/src/Kook.Net.Rest/API/Common/Ban.cs index 4ec039f8..562fd453 100644 --- a/src/Kook.Net.Rest/API/Common/Ban.cs +++ b/src/Kook.Net.Rest/API/Common/Ban.cs @@ -6,15 +6,15 @@ namespace Kook.API; internal class Ban { [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } [JsonPropertyName("created_time")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset CreatedAt { get; set; } + public required DateTimeOffset CreatedAt { get; set; } [JsonPropertyName("remark")] - public string Reason { get; set; } + public required string Reason { get; set; } [JsonPropertyName("user")] - public User User { get; set; } + public required User User { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Card.cs b/src/Kook.Net.Rest/API/Common/Cards/Card.cs index e9e4c126..657e4331 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Card.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Card.cs @@ -7,7 +7,7 @@ internal class Card : CardBase { [JsonPropertyName("theme")] [JsonConverter(typeof(CardThemeConverter))] - public CardTheme Theme { get; set; } + public CardTheme? Theme { get; set; } [JsonPropertyName("color")] [JsonConverter(typeof(HexColorConverter))] @@ -16,8 +16,8 @@ internal class Card : CardBase [JsonPropertyName("size")] [JsonConverter(typeof(CardSizeConverter))] - public CardSize Size { get; set; } + public CardSize? Size { get; set; } [JsonPropertyName("modules")] - public ModuleBase[] Modules { get; set; } + public required ModuleBase[] Modules { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/CardBase.cs b/src/Kook.Net.Rest/API/Common/Cards/CardBase.cs index d6c6f486..a4ad0ff5 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/CardBase.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/CardBase.cs @@ -7,5 +7,5 @@ internal class CardBase : ICard { [JsonPropertyName("type")] [JsonConverter(typeof(CardTypeConverter))] - public CardType Type { get; set; } + public required CardType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/ButtonElement.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/ButtonElement.cs index d2ac84a3..bb0cb811 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/ButtonElement.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/ButtonElement.cs @@ -7,15 +7,15 @@ internal class ButtonElement : ElementBase { [JsonPropertyName("theme")] [JsonConverter(typeof(ButtonThemeConverter))] - public ButtonTheme Theme { get; set; } + public ButtonTheme? Theme { get; set; } [JsonPropertyName("value")] - public string Value { get; set; } + public string? Value { get; set; } [JsonPropertyName("click")] [JsonConverter(typeof(ButtonClickEventTypeConverter))] - public ButtonClickEventType Click { get; set; } + public ButtonClickEventType? Click { get; set; } [JsonPropertyName("text")] - public ElementBase Text { get; set; } + public required ElementBase Text { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/ElementBase.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/ElementBase.cs index 399269a0..7f6b569d 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/ElementBase.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/ElementBase.cs @@ -7,5 +7,5 @@ internal class ElementBase : IElement { [JsonPropertyName("type")] [JsonConverter(typeof(ElementTypeConverter))] - public ElementType Type { get; set; } + public required ElementType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/ImageElement.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/ImageElement.cs index 4ea02b87..5708ee23 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/ImageElement.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/ImageElement.cs @@ -6,10 +6,10 @@ namespace Kook.API; internal class ImageElement : ElementBase { [JsonPropertyName("src")] - public string Source { get; set; } + public required string Source { get; set; } [JsonPropertyName("alt")] - public string Alternative { get; set; } + public string? Alternative { get; set; } [JsonPropertyName("size")] [JsonConverter(typeof(ImageSizeConverter))] diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/KMarkdownElement.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/KMarkdownElement.cs index 4c36211f..69cbef3f 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/KMarkdownElement.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/KMarkdownElement.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class KMarkdownElement : ElementBase { [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/ParagraphStruct.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/ParagraphStruct.cs index c692857a..2377811c 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/ParagraphStruct.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/ParagraphStruct.cs @@ -5,8 +5,8 @@ namespace Kook.API; internal class ParagraphStruct : ElementBase { [JsonPropertyName("cols")] - public int ColumnCount { get; set; } + public int? ColumnCount { get; set; } [JsonPropertyName("fields")] - public ElementBase[] Fields { get; set; } + public ElementBase[]? Fields { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Elements/PlainTextElement.cs b/src/Kook.Net.Rest/API/Common/Cards/Elements/PlainTextElement.cs index 135685d6..f61c9f12 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Elements/PlainTextElement.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Elements/PlainTextElement.cs @@ -5,8 +5,8 @@ namespace Kook.API; internal class PlainTextElement : ElementBase { [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("emoji")] - public bool Emoji { get; set; } + public bool? Emoji { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/ActionGroupModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/ActionGroupModule.cs index f9a2417e..a1c74e32 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/ActionGroupModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/ActionGroupModule.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class ActionGroupModule : ModuleBase { [JsonPropertyName("elements")] - public ButtonElement[] Elements { get; set; } + public required ButtonElement[] Elements { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/AudioModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/AudioModule.cs index 0a79a4d8..64c39ab7 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/AudioModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/AudioModule.cs @@ -5,11 +5,11 @@ namespace Kook.API; internal class AudioModule : ModuleBase { [JsonPropertyName("src")] - public string Source { get; set; } + public required string Source { get; set; } [JsonPropertyName("title")] - public string Title { get; set; } + public string? Title { get; set; } [JsonPropertyName("cover")] - public string Cover { get; set; } + public string? Cover { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/ContainerModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/ContainerModule.cs index 08bd4dd4..35aead98 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/ContainerModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/ContainerModule.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class ContainerModule : ModuleBase { [JsonPropertyName("elements")] - public ImageElement[] Elements { get; set; } + public required ImageElement[] Elements { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/ContextModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/ContextModule.cs index f132152a..d77846fc 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/ContextModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/ContextModule.cs @@ -5,5 +5,6 @@ namespace Kook.API; internal class ContextModule : ModuleBase { [JsonPropertyName("elements")] - public ElementBase[] Elements { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public ElementBase[]? Elements { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs index a4864c41..9a92ca68 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs @@ -5,8 +5,8 @@ namespace Kook.API; internal class FileModule : ModuleBase { [JsonPropertyName("src")] - public string Source { get; set; } + public required string Source { get; set; } [JsonPropertyName("title")] - public string Title { get; set; } + public string? Title { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/HeaderModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/HeaderModule.cs index d514b338..ed06929d 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/HeaderModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/HeaderModule.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class HeaderModule : ModuleBase { [JsonPropertyName("text")] - public PlainTextElement Text { get; set; } + public PlainTextElement? Text { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/ImageGroupModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/ImageGroupModule.cs index e809393f..b3b5d37f 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/ImageGroupModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/ImageGroupModule.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class ImageGroupModule : ModuleBase { [JsonPropertyName("elements")] - public ImageElement[] Elements { get; set; } + public required ImageElement[] Elements { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/InviteModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/InviteModule.cs index a74dd741..08db3954 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/InviteModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/InviteModule.cs @@ -5,5 +5,5 @@ namespace Kook.API; internal class InviteModule : ModuleBase { [JsonPropertyName("code")] - public string Code { get; set; } + public string? Code { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/ModuleBase.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/ModuleBase.cs index fdbbf79c..e5c7a478 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/ModuleBase.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/ModuleBase.cs @@ -7,5 +7,5 @@ internal class ModuleBase : IModule { [JsonPropertyName("type")] [JsonConverter(typeof(ModuleTypeConverter))] - public ModuleType Type { get; set; } + public required ModuleType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/SectionModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/SectionModule.cs index 74187ecc..3f27ec74 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/SectionModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/SectionModule.cs @@ -7,12 +7,12 @@ internal class SectionModule : ModuleBase { [JsonPropertyName("mode")] [JsonConverter(typeof(SectionAccessoryModeConverter))] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public SectionAccessoryMode Mode { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SectionAccessoryMode? Mode { get; set; } [JsonPropertyName("text")] - public ElementBase Text { get; set; } + public ElementBase? Text { get; set; } [JsonPropertyName("accessory")] - public ElementBase Accessory { get; set; } + public ElementBase? Accessory { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/VideoModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/VideoModule.cs index 6e161ef3..5c7785ac 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/VideoModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/VideoModule.cs @@ -5,8 +5,8 @@ namespace Kook.API; internal class VideoModule : ModuleBase { [JsonPropertyName("src")] - public string Source { get; set; } + public required string Source { get; set; } [JsonPropertyName("title")] - public string Title { get; set; } + public string? Title { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Channel.cs b/src/Kook.Net.Rest/API/Common/Channel.cs index 9f766a14..b3924ca8 100644 --- a/src/Kook.Net.Rest/API/Common/Channel.cs +++ b/src/Kook.Net.Rest/API/Common/Channel.cs @@ -9,7 +9,7 @@ internal class Channel public ulong Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("user_id")] public ulong CreatorId { get; set; } @@ -31,13 +31,13 @@ internal class Channel public ChannelType Type { get; set; } [JsonPropertyName("permission_overwrites")] - public RolePermissionOverwrite[] RolePermissionOverwrites { get; set; } + public required RolePermissionOverwrite[] RolePermissionOverwrites { get; set; } [JsonPropertyName("permission_users")] - public UserPermissionOverwrite[] UserPermissionOverwrites { get; set; } + public required UserPermissionOverwrite[] UserPermissionOverwrites { get; set; } [JsonPropertyName("channels")] - public Channel[] Channels { get; set; } + public Channel[]? Channels { get; set; } [JsonPropertyName("permission_sync")] [JsonConverter(typeof(NumberBooleanConverter))] @@ -45,21 +45,21 @@ internal class Channel // Text [JsonPropertyName("topic")] - public string Topic { get; set; } + public string? Topic { get; set; } [JsonPropertyName("slow_mode")] public int SlowMode { get; set; } // Voice [JsonPropertyName("limit_amount")] - public int? UserLimit { get; set; } + public int UserLimit { get; set; } [JsonPropertyName("voice_quality")] [JsonConverter(typeof(NullableVoiceQualityConverter))] public VoiceQuality? VoiceQuality { get; set; } [JsonPropertyName("server_url")] - public string ServerUrl { get; set; } + public string? ServerUrl { get; set; } [JsonPropertyName("has_password")] public bool HasPassword { get; set; } @@ -70,5 +70,5 @@ internal class Channel public bool? OverwriteVoiceRegion { get; set; } [JsonPropertyName("region")] - public string VoiceRegion { get; set; } + public string? VoiceRegion { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/DirectMessage.cs b/src/Kook.Net.Rest/API/Common/DirectMessage.cs index fdd65f6a..207ba1a2 100644 --- a/src/Kook.Net.Rest/API/Common/DirectMessage.cs +++ b/src/Kook.Net.Rest/API/Common/DirectMessage.cs @@ -12,38 +12,42 @@ internal class DirectMessage public MessageType Type { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("embeds")] - public EmbedBase[] Embeds { get; set; } + public required EmbedBase[] Embeds { get; set; } - [JsonPropertyName("attachment")] - public Attachment Attachment { get; set; } + [JsonPropertyName("attachments")] + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } [JsonPropertyName("create_at")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset CreateAt { get; set; } [JsonPropertyName("update_at")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset UpdateAt { get; set; } + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset? UpdateAt { get; set; } [JsonPropertyName("reactions")] - public Reaction[] Reactions { get; set; } + public required Reaction[] Reactions { get; set; } [JsonPropertyName("author_id")] public ulong AuthorId { get; set; } + [JsonPropertyName("author")] + public User? Author { get; set; } + [JsonPropertyName("image_name")] - public string ImageName { get; set; } + public required string ImageName { get; set; } [JsonPropertyName("read_status")] public bool ReadStatus { get; set; } [JsonPropertyName("quote")] [JsonConverter(typeof(QuoteConverter))] - public Quote Quote { get; set; } + public Quote? Quote { get; set; } [JsonPropertyName("mention_info")] - public MentionInfo MentionInfo { get; set; } + public MentionInfo? MentionInfo { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/BilibiliVideoEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/BilibiliVideoEmbed.cs index edbc1e99..0d1a3b2f 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/BilibiliVideoEmbed.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/BilibiliVideoEmbed.cs @@ -5,20 +5,23 @@ namespace Kook.API; internal class BilibiliVideoEmbed : EmbedBase { [JsonPropertyName("origin_url")] - public string OriginUrl { get; set; } + public required string OriginUrl { get; set; } [JsonPropertyName("av_no")] - public string BvNumber { get; set; } + public required string BvNumber { get; set; } [JsonPropertyName("iframe_path")] - public string IframePath { get; set; } + public required string IframePath { get; set; } [JsonPropertyName("duration")] - public int Duration { get; set; } + public required int Duration { get; set; } [JsonPropertyName("title")] - public string Title { get; set; } + public required string Title { get; set; } [JsonPropertyName("pic")] - public string Cover { get; set; } + public required string Cover { get; set; } + + [JsonPropertyName("url")] + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/CardEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/CardEmbed.cs new file mode 100644 index 00000000..a862e01a --- /dev/null +++ b/src/Kook.Net.Rest/API/Common/Embeds/CardEmbed.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using Kook.Net.Converters; + +namespace Kook.API; + +internal class CardEmbed : EmbedBase +{ + [JsonPropertyName("theme")] + [JsonConverter(typeof(CardThemeConverter))] + public CardTheme? Theme { get; set; } + + [JsonPropertyName("color")] + [JsonConverter(typeof(HexColorConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Color? Color { get; set; } + + [JsonPropertyName("size")] + [JsonConverter(typeof(CardSizeConverter))] + public CardSize? Size { get; set; } + + [JsonPropertyName("modules")] + public required ModuleBase[] Modules { get; set; } +} diff --git a/src/Kook.Net.Rest/API/Common/Embeds/EmbedBase.cs b/src/Kook.Net.Rest/API/Common/Embeds/EmbedBase.cs index 1c776a6f..1f65867b 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/EmbedBase.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/EmbedBase.cs @@ -9,7 +9,4 @@ internal class EmbedBase : IEmbed [JsonPropertyName("type")] [JsonConverter(typeof(EmbedTypeConverter))] public EmbedType Type { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/IEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/IEmbed.cs index a8f82056..2bf5cd14 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/IEmbed.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/IEmbed.cs @@ -3,6 +3,4 @@ namespace Kook.API; internal interface IEmbed { EmbedType Type { get; } - - string Url { get; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/ImageEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/ImageEmbed.cs index 68a5ac6c..ddc3506a 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/ImageEmbed.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/ImageEmbed.cs @@ -5,5 +5,8 @@ namespace Kook.API; internal class ImageEmbed : EmbedBase { [JsonPropertyName("origin_url")] - public string OriginUrl { get; set; } + public required string OriginUrl { get; set; } + + [JsonPropertyName("url")] + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/LinkEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/LinkEmbed.cs index c138dfe3..bd39aefc 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/LinkEmbed.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/LinkEmbed.cs @@ -6,18 +6,21 @@ namespace Kook.API; internal class LinkEmbed : EmbedBase { [JsonPropertyName("title")] - public string Title { get; set; } + public required string Title { get; set; } [JsonPropertyName("description")] - public string Description { get; set; } + public required string Description { get; set; } [JsonPropertyName("site_name")] - public string SiteName { get; set; } + public required string SiteName { get; set; } [JsonPropertyName("theme_color")] [JsonConverter(typeof(HexColorConverter))] - public Color Color { get; set; } + public required Color Color { get; set; } [JsonPropertyName("image")] - public string Image { get; set; } + public required string Image { get; set; } + + [JsonPropertyName("url")] + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Embeds/NotImplementedEmbed.cs b/src/Kook.Net.Rest/API/Common/Embeds/NotImplementedEmbed.cs index f1c04ca2..c7250982 100644 --- a/src/Kook.Net.Rest/API/Common/Embeds/NotImplementedEmbed.cs +++ b/src/Kook.Net.Rest/API/Common/Embeds/NotImplementedEmbed.cs @@ -4,10 +4,9 @@ namespace Kook.API; internal class NotImplementedEmbed : EmbedBase { - internal NotImplementedEmbed(string rawType, string url, JsonNode rawJsonNode) + internal NotImplementedEmbed(string rawType, JsonNode rawJsonNode) { RawType = rawType; - Url = url; RawJsonNode = rawJsonNode; } diff --git a/src/Kook.Net.Rest/API/Common/Emoji.cs b/src/Kook.Net.Rest/API/Common/Emoji.cs index 90f28034..454e144a 100644 --- a/src/Kook.Net.Rest/API/Common/Emoji.cs +++ b/src/Kook.Net.Rest/API/Common/Emoji.cs @@ -4,15 +4,15 @@ namespace Kook.API; internal class Emoji { - [JsonPropertyName("emoji_type")] - public EmojiType Type { get; set; } - [JsonPropertyName("id")] - public string Id { get; set; } + public required string Id { get; set; } + + [JsonPropertyName("emoji_type")] + public EmojiType? Type { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("user_info")] - public User UploadedBy { get; set; } + public User? UploadedBy { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Game.cs b/src/Kook.Net.Rest/API/Common/Game.cs index d8cfced9..69577878 100644 --- a/src/Kook.Net.Rest/API/Common/Game.cs +++ b/src/Kook.Net.Rest/API/Common/Game.cs @@ -8,23 +8,23 @@ internal class Game public int Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("type")] public GameType Type { get; set; } [JsonPropertyName("options")] - public string Options { get; set; } + public string? Options { get; set; } [JsonPropertyName("kmhook_admin")] public bool KmHookAdmin { get; set; } + [JsonPropertyName("icon")] + public required string Icon { get; set; } + [JsonPropertyName("process_name")] - public string[] ProcessNames { get; set; } + public required string[] ProcessNames { get; set; } [JsonPropertyName("product_name")] - public string[] ProductNames { get; set; } - - [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string[] ProductNames { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Guild.cs b/src/Kook.Net.Rest/API/Common/Guild.cs index 7168143d..151684d0 100644 --- a/src/Kook.Net.Rest/API/Common/Guild.cs +++ b/src/Kook.Net.Rest/API/Common/Guild.cs @@ -9,22 +9,22 @@ internal class Guild public ulong Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("topic")] - public string Topic { get; set; } + public required string Topic { get; set; } [JsonPropertyName("user_id")] public ulong OwnerId { get; set; } [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string Icon { get; set; } [JsonPropertyName("notify_type")] public NotifyType NotifyType { get; set; } [JsonPropertyName("region")] - public string Region { get; set; } + public required string Region { get; set; } [JsonPropertyName("enable_open")] [JsonConverter(typeof(NumberBooleanConverter))] @@ -44,8 +44,8 @@ internal class Guild public ulong WelcomeChannelId { get; set; } [JsonPropertyName("roles")] - public Role[] Roles { get; set; } + public Role[]? Roles { get; set; } [JsonPropertyName("channels")] - public Channel[] Channels { get; set; } + public Channel[]? Channels { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/GuildMentionInfo.cs b/src/Kook.Net.Rest/API/Common/GuildMentionInfo.cs new file mode 100644 index 00000000..88ffcd71 --- /dev/null +++ b/src/Kook.Net.Rest/API/Common/GuildMentionInfo.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Kook.API; + +internal class GuildMentionInfo : MentionInfo +{ + [JsonPropertyName("mention_role_part")] + public required Role[] MentionedRoles { get; set; } +} diff --git a/src/Kook.Net.Rest/API/Common/Intimacy.cs b/src/Kook.Net.Rest/API/Common/Intimacy.cs index a89bacaf..ddd9af4c 100644 --- a/src/Kook.Net.Rest/API/Common/Intimacy.cs +++ b/src/Kook.Net.Rest/API/Common/Intimacy.cs @@ -6,24 +6,24 @@ namespace Kook.API; internal class Intimacy { [JsonPropertyName("img_url")] - public string ImageUrl { get; set; } + public required string ImageUrl { get; set; } [JsonPropertyName("social_info")] - public string SocialInfo { get; set; } + public required string SocialInfo { get; set; } [JsonPropertyName("last_read")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset LastReadAt { get; set; } [JsonPropertyName("last_modify")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset LastModifyAt { get; set; } + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset? LastModifyAt { get; set; } [JsonPropertyName("score")] public int Score { get; set; } [JsonPropertyName("img_list")] - public IntimacyImage[] Images { get; set; } + public required IntimacyImage[] Images { get; set; } } internal class IntimacyImage @@ -32,5 +32,5 @@ internal class IntimacyImage public uint Id { get; set; } [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Invite.cs b/src/Kook.Net.Rest/API/Common/Invite.cs index 30b222fb..8ea8ab90 100644 --- a/src/Kook.Net.Rest/API/Common/Invite.cs +++ b/src/Kook.Net.Rest/API/Common/Invite.cs @@ -9,32 +9,31 @@ internal class Invite public uint Id { get; set; } [JsonPropertyName("channel_id")] - [JsonConverter(typeof(NullableUInt64Converter))] - public ulong? ChannelId { get; set; } + public ulong ChannelId { get; set; } [JsonPropertyName("guild_id")] public ulong GuildId { get; set; } [JsonPropertyName("guild_name")] - public string GuildName { get; set; } + public required string GuildName { get; set; } [JsonPropertyName("channel_name")] - public string ChannelName { get; set; } + public string? ChannelName { get; set; } [JsonPropertyName("type")] public ChannelType ChannelType { get; set; } [JsonPropertyName("url_code")] - public string UrlCode { get; set; } + public required string UrlCode { get; set; } [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } [JsonPropertyName("user")] - public User Inviter { get; set; } + public required User Inviter { get; set; } [JsonPropertyName("expire_time")] - [JsonConverter(typeof(NullableDateTimeOffsetConverter))] + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset? ExpiresAt { get; set; } [JsonPropertyName("remaining_times")] @@ -46,4 +45,11 @@ internal class Invite [JsonPropertyName("duration")] [JsonConverter(typeof(NullableTimeSpanConverter))] public TimeSpan? Duration { get; set; } + + [JsonPropertyName("invitees_count")] + public int InviteesCount { get; set; } + + [JsonPropertyName("created_at")] + [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset CreatedAt { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/MentionInfo.cs b/src/Kook.Net.Rest/API/Common/MentionInfo.cs index f2fbe5aa..9afb97ac 100644 --- a/src/Kook.Net.Rest/API/Common/MentionInfo.cs +++ b/src/Kook.Net.Rest/API/Common/MentionInfo.cs @@ -5,14 +5,11 @@ namespace Kook.API; internal class MentionInfo { [JsonPropertyName("mention_part")] - public MentionedUser[] MentionedUsers { get; set; } - - [JsonPropertyName("mention_role_part")] - public Role[] MentionedRoles { get; set; } + public required MentionedUser[] MentionedUsers { get; set; } [JsonPropertyName("channel_part")] - public MentionedChannel[] MentionedChannels { get; set; } + public required MentionedChannel[] MentionedChannels { get; set; } [JsonPropertyName("item_part")] - public Poke[] Pokes { get; set; } + public required Poke[] Pokes { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/MentionedChannel.cs b/src/Kook.Net.Rest/API/Common/MentionedChannel.cs index eb3f3180..f364a7e5 100644 --- a/src/Kook.Net.Rest/API/Common/MentionedChannel.cs +++ b/src/Kook.Net.Rest/API/Common/MentionedChannel.cs @@ -11,5 +11,5 @@ internal class MentionedChannel public ulong GuildId { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/MentionedUser.cs b/src/Kook.Net.Rest/API/Common/MentionedUser.cs index 7d3addb5..f8aead7d 100644 --- a/src/Kook.Net.Rest/API/Common/MentionedUser.cs +++ b/src/Kook.Net.Rest/API/Common/MentionedUser.cs @@ -8,11 +8,11 @@ internal class MentionedUser public ulong Id { get; set; } [JsonPropertyName("username")] - public string Username { get; set; } + public required string DisplayName { get; set; } [JsonPropertyName("full_name")] - public string FullName { get; set; } + public required string FullName { get; set; } [JsonPropertyName("avatar")] - public string Avatar { get; set; } + public required string Avatar { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Message.cs b/src/Kook.Net.Rest/API/Common/Message.cs index 04e27737..b7428530 100644 --- a/src/Kook.Net.Rest/API/Common/Message.cs +++ b/src/Kook.Net.Rest/API/Common/Message.cs @@ -12,50 +12,54 @@ internal class Message public MessageType Type { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("mention")] - public ulong[] MentionedUsers { get; set; } + public ulong[]? MentionedUsers { get; set; } [JsonPropertyName("mention_all")] public bool MentionedAll { get; set; } [JsonPropertyName("mention_roles")] - public uint[] MentionedRoles { get; set; } + public required uint[] MentionedRoles { get; set; } [JsonPropertyName("mention_here")] public bool MentionedHere { get; set; } [JsonPropertyName("embeds")] - public EmbedBase[] Embeds { get; set; } + public required EmbedBase[] Embeds { get; set; } - [JsonPropertyName("attachment")] - public Attachment Attachment { get; set; } + [JsonPropertyName("attachments")] + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } [JsonPropertyName("create_at")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset CreateAt { get; set; } [JsonPropertyName("updated_at")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset UpdateAt { get; set; } + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset? UpdateAt { get; set; } [JsonPropertyName("reactions")] - public Reaction[] Reactions { get; set; } + public required Reaction[] Reactions { get; set; } [JsonPropertyName("author")] - public User Author { get; set; } + public required User Author { get; set; } [JsonPropertyName("image_name")] - public string ImageName { get; set; } + public required string ImageName { get; set; } [JsonPropertyName("read_status")] public bool ReadStatus { get; set; } [JsonPropertyName("quote")] [JsonConverter(typeof(QuoteConverter))] - public Quote Quote { get; set; } + public Quote? Quote { get; set; } [JsonPropertyName("mention_info")] - public MentionInfo MentionInfo { get; set; } + public GuildMentionInfo? MentionInfo { get; set; } + + [JsonPropertyName("editable")] + public bool? Editable { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Nameplate.cs b/src/Kook.Net.Rest/API/Common/Nameplate.cs index 0153f880..198c1ada 100644 --- a/src/Kook.Net.Rest/API/Common/Nameplate.cs +++ b/src/Kook.Net.Rest/API/Common/Nameplate.cs @@ -5,14 +5,20 @@ namespace Kook.API; internal class Nameplate { [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("type")] - public int Type { get; set; } + public required int Type { get; set; } [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string Icon { get; set; } [JsonPropertyName("tips")] - public string Tips { get; set; } + public required string Tips { get; set; } + + [JsonPropertyName("h")] + public int? Height { get; set; } + + [JsonPropertyName("w")] + public int? Width { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Pokes/ImageAnimationPokeResource.cs b/src/Kook.Net.Rest/API/Common/Pokes/ImageAnimationPokeResource.cs index 40c08c13..0283330e 100644 --- a/src/Kook.Net.Rest/API/Common/Pokes/ImageAnimationPokeResource.cs +++ b/src/Kook.Net.Rest/API/Common/Pokes/ImageAnimationPokeResource.cs @@ -5,26 +5,26 @@ namespace Kook.API; internal class ImageAnimationPokeResource : PokeResourceBase { [JsonPropertyName("preview_expired")] - public string PreviewExpired { get; set; } + public required string PreviewExpired { get; set; } [JsonPropertyName("webp")] - public string WebP { get; set; } + public required string WebP { get; set; } [JsonPropertyName("pag")] - public string PAG { get; set; } + public required string PAG { get; set; } [JsonPropertyName("gif")] - public string GIF { get; set; } + public required string GIF { get; set; } [JsonPropertyName("time")] - public int Duration { get; set; } + public required int Duration { get; set; } [JsonPropertyName("width")] - public int Width { get; set; } + public required int Width { get; set; } [JsonPropertyName("height")] - public int Height { get; set; } + public required int Height { get; set; } [JsonPropertyName("percent")] - public int Percent { get; set; } + public required int Percent { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Pokes/Poke.cs b/src/Kook.Net.Rest/API/Common/Pokes/Poke.cs index 757d74aa..01b8faa9 100644 --- a/src/Kook.Net.Rest/API/Common/Pokes/Poke.cs +++ b/src/Kook.Net.Rest/API/Common/Pokes/Poke.cs @@ -5,41 +5,41 @@ namespace Kook.API; internal class Poke { [JsonPropertyName("id")] - public uint Id { get; set; } + public required uint Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("desc")] - public string Description { get; set; } + public required string Description { get; set; } [JsonPropertyName("cd")] - public int Cooldown { get; set; } + public required int Cooldown { get; set; } [JsonPropertyName("categories")] - public string[] Categories { get; set; } + public required string[] Categories { get; set; } [JsonPropertyName("label")] - public uint LabelId { get; set; } + public required uint LabelId { get; set; } [JsonPropertyName("label_name")] - public string LabelName { get; set; } + public required string LabelName { get; set; } [JsonPropertyName("quality")] - public uint QualityId { get; set; } + public required uint QualityId { get; set; } [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string Icon { get; set; } [JsonPropertyName("icon_expired")] - public string IconExpired { get; set; } + public required string IconExpired { get; set; } [JsonPropertyName("quality_resource")] - public PokeQualityResource Quality { get; set; } + public required PokeQualityResource Quality { get; set; } [JsonPropertyName("resources")] - public PokeResourceBase Resource { get; set; } + public required PokeResourceBase Resource { get; set; } [JsonPropertyName("msg_scenarios")] - public Dictionary MessageScenarios { get; set; } + public required Dictionary MessageScenarios { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Pokes/PokeQualityResource.cs b/src/Kook.Net.Rest/API/Common/Pokes/PokeQualityResource.cs index 3774527c..dd914755 100644 --- a/src/Kook.Net.Rest/API/Common/Pokes/PokeQualityResource.cs +++ b/src/Kook.Net.Rest/API/Common/Pokes/PokeQualityResource.cs @@ -7,11 +7,11 @@ internal class PokeQualityResource { [JsonPropertyName("color")] [JsonConverter(typeof(HexColorConverter))] - public Color Color { get; set; } + public required Color Color { get; set; } [JsonPropertyName("small")] - public string Small { get; set; } + public required string Small { get; set; } [JsonPropertyName("big")] - public string Big { get; set; } + public required string Big { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Pokes/PokeResourceBase.cs b/src/Kook.Net.Rest/API/Common/Pokes/PokeResourceBase.cs index 4864de07..22068e89 100644 --- a/src/Kook.Net.Rest/API/Common/Pokes/PokeResourceBase.cs +++ b/src/Kook.Net.Rest/API/Common/Pokes/PokeResourceBase.cs @@ -8,5 +8,5 @@ internal class PokeResourceBase : IPokeResource { [JsonPropertyName("type")] [JsonConverter(typeof(PokeResourceTypeConverter))] - public PokeResourceType Type { get; set; } + public required PokeResourceType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Quote.cs b/src/Kook.Net.Rest/API/Common/Quote.cs index 7c77a985..d3877c0c 100644 --- a/src/Kook.Net.Rest/API/Common/Quote.cs +++ b/src/Kook.Net.Rest/API/Common/Quote.cs @@ -6,21 +6,26 @@ namespace Kook.API; internal class Quote { [JsonPropertyName("id")] - public string Id { get; set; } + [JsonConverter(typeof(NullableGuidConverter))] + public Guid? QuotedMessageId { get; set; } + // TODO: To be investigated [JsonPropertyName("rong_id")] - public Guid QuotedMessageId { get; set; } + public Guid? RongId { get; set; } [JsonPropertyName("type")] public MessageType Type { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("create_at")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset CreateAt { get; set; } [JsonPropertyName("author")] - public User Author { get; set; } + public required User Author { get; set; } + + [JsonPropertyName("can_jump")] + public bool CanJump { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Reaction.cs b/src/Kook.Net.Rest/API/Common/Reaction.cs index a3bc5fec..d7334f37 100644 --- a/src/Kook.Net.Rest/API/Common/Reaction.cs +++ b/src/Kook.Net.Rest/API/Common/Reaction.cs @@ -5,7 +5,7 @@ namespace Kook.API; internal class Reaction { [JsonPropertyName("emoji")] - public Emoji Emoji { get; set; } + public required Emoji Emoji { get; set; } [JsonPropertyName("count")] public int Count { get; set; } diff --git a/src/Kook.Net.Rest/API/Common/RecommendInfo.cs b/src/Kook.Net.Rest/API/Common/RecommendInfo.cs index b4fb2b0a..f4031e0c 100644 --- a/src/Kook.Net.Rest/API/Common/RecommendInfo.cs +++ b/src/Kook.Net.Rest/API/Common/RecommendInfo.cs @@ -9,38 +9,39 @@ internal class RecommendInfo public ulong GuildId { get; set; } [JsonPropertyName("open_id")] - public uint OpenId { get; set; } + [JsonConverter(typeof(NullableUInt32Converter))] + public uint? OpenId { get; set; } [JsonPropertyName("default_channel_id")] public ulong DefaultChannelId { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string Icon { get; set; } [JsonPropertyName("banner")] - public string Banner { get; set; } + public required string Banner { get; set; } [JsonPropertyName("desc")] - public string Description { get; set; } + public required string Description { get; set; } [JsonPropertyName("status")] public int Status { get; set; } [JsonPropertyName("tag")] - public string Tag { get; set; } + public required string Tag { get; set; } [JsonPropertyName("features")] [JsonConverter(typeof(GuildFeaturesConverter))] - public GuildFeatures Features { get; set; } + public required GuildFeatures Features { get; set; } [JsonPropertyName("level")] public BoostLevel BoostLevel { get; set; } [JsonPropertyName("custom_id")] - public string CustomId { get; set; } + public required string CustomId { get; set; } [JsonPropertyName("is_official_partner")] [JsonConverter(typeof(NumberBooleanConverter))] @@ -53,6 +54,5 @@ internal class RecommendInfo public int AuditStatus { get; set; } [JsonPropertyName("update_day_gap")] - public int UpdateDayInterval { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/Role.cs b/src/Kook.Net.Rest/API/Common/Role.cs index 0eecec3a..b22d34d4 100644 --- a/src/Kook.Net.Rest/API/Common/Role.cs +++ b/src/Kook.Net.Rest/API/Common/Role.cs @@ -9,7 +9,7 @@ internal class Role public uint Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("color")] [JsonConverter(typeof(RawValueColorConverter))] @@ -37,5 +37,5 @@ internal class Role public ulong Permissions { get; set; } [JsonPropertyName("type")] - public RoleType? Type { get; set; } + public RoleType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/User.cs b/src/Kook.Net.Rest/API/Common/User.cs index d9f22753..35690157 100644 --- a/src/Kook.Net.Rest/API/Common/User.cs +++ b/src/Kook.Net.Rest/API/Common/User.cs @@ -8,16 +8,16 @@ internal class User public ulong Id { get; set; } [JsonPropertyName("username")] - public string Username { get; set; } + public required string Username { get; set; } [JsonPropertyName("identify_num")] - public string IdentifyNumber { get; set; } + public required string IdentifyNumber { get; set; } [JsonPropertyName("online")] public bool Online { get; set; } [JsonPropertyName("os")] - public string OperatingSystem { get; set; } + public string? OperatingSystem { get; set; } [JsonPropertyName("bot")] public bool? Bot { get; set; } @@ -26,10 +26,10 @@ internal class User public int? Status { get; set; } [JsonPropertyName("avatar")] - public string Avatar { get; set; } + public required string Avatar { get; set; } [JsonPropertyName("vip_avatar")] - public string BuffAvatar { get; set; } + public required string BuffAvatar { get; set; } [JsonPropertyName("is_vip")] public bool? HasBuff { get; set; } @@ -41,13 +41,13 @@ internal class User public bool? IsDenoiseEnabled { get; set; } [JsonPropertyName("tag_info")] - public UserTag UserTag { get; set; } + public UserTag? UserTag { get; set; } [JsonPropertyName("banner")] - public string Banner { get; set; } + public string? Banner { get; set; } [JsonPropertyName("nameplate")] - public Nameplate[] Nameplates { get; set; } + public Nameplate[]? Nameplates { get; set; } [JsonPropertyName("is_sys")] public bool? IsSystemUser { get; set; } diff --git a/src/Kook.Net.Rest/API/Common/UserChat.cs b/src/Kook.Net.Rest/API/Common/UserChat.cs index eef36dea..1bbebb1e 100644 --- a/src/Kook.Net.Rest/API/Common/UserChat.cs +++ b/src/Kook.Net.Rest/API/Common/UserChat.cs @@ -21,14 +21,5 @@ internal class UserChat public int UnreadCount { get; set; } [JsonPropertyName("target_info")] - public User Recipient { get; set; } - - [JsonPropertyName("is_friend")] - public bool? IsFriend { get; set; } - - [JsonPropertyName("is_blocked")] - public bool? IsBlocked { get; set; } - - [JsonPropertyName("is_target_blocked")] - public bool? IsTargetBlocked { get; set; } + public required User Recipient { get; set; } } diff --git a/src/Kook.Net.Rest/API/Common/UserPermissionOverwrite.cs b/src/Kook.Net.Rest/API/Common/UserPermissionOverwrite.cs index 0cc301d2..de3b8150 100644 --- a/src/Kook.Net.Rest/API/Common/UserPermissionOverwrite.cs +++ b/src/Kook.Net.Rest/API/Common/UserPermissionOverwrite.cs @@ -5,7 +5,7 @@ namespace Kook.API; internal class UserPermissionOverwrite { [JsonPropertyName("user")] - public User User { get; set; } + public required User User { get; set; } [JsonPropertyName("allow")] public ulong Allow { get; set; } diff --git a/src/Kook.Net.Rest/API/Common/UserTag.cs b/src/Kook.Net.Rest/API/Common/UserTag.cs index d6f1644a..76c2daf3 100644 --- a/src/Kook.Net.Rest/API/Common/UserTag.cs +++ b/src/Kook.Net.Rest/API/Common/UserTag.cs @@ -14,5 +14,5 @@ internal class UserTag public AlphaColor BackgroundColor { get; set; } [JsonPropertyName("text")] - public string Text { get; set; } + public required string Text { get; set; } } diff --git a/src/Kook.Net.Rest/API/Net/MultipartFile.cs b/src/Kook.Net.Rest/API/Net/MultipartFile.cs index 8c285c53..b67c9140 100644 --- a/src/Kook.Net.Rest/API/Net/MultipartFile.cs +++ b/src/Kook.Net.Rest/API/Net/MultipartFile.cs @@ -3,10 +3,10 @@ namespace Kook.Net.Rest; internal struct MultipartFile { public Stream Stream { get; } - public string Filename { get; } - public string ContentType { get; } + public string? Filename { get; } + public string? ContentType { get; } - public MultipartFile(Stream stream, string filename, string contentType = null) + public MultipartFile(Stream stream, string? filename, string? contentType = null) { Stream = stream; Filename = filename; diff --git a/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleParams.cs b/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleParams.cs index e891e41d..9593e380 100644 --- a/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleParams.cs +++ b/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleParams.cs @@ -5,11 +5,11 @@ namespace Kook.API.Rest; internal class AddOrRemoveRoleParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } [JsonPropertyName("role_id")] - public uint RoleId { get; set; } + public required uint RoleId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleResponsecs.cs b/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleResponsecs.cs index 0553b44c..8e66afdd 100644 --- a/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleResponsecs.cs +++ b/src/Kook.Net.Rest/API/Rest/AddOrRemoveRoleResponsecs.cs @@ -11,5 +11,5 @@ internal class AddOrRemoveRoleResponse public ulong UserId { get; set; } [JsonPropertyName("roles")] - public uint[] RoleIds { get; set; } + public required uint[] RoleIds { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/AddReactionParams.cs b/src/Kook.Net.Rest/API/Rest/AddReactionParams.cs index 45ad47e2..f342bc0f 100644 --- a/src/Kook.Net.Rest/API/Rest/AddReactionParams.cs +++ b/src/Kook.Net.Rest/API/Rest/AddReactionParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class AddReactionParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } [JsonPropertyName("emoji")] - public string EmojiId { get; set; } + public required string EmojiId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/BeginActivityParams.cs b/src/Kook.Net.Rest/API/Rest/BeginActivityParams.cs index af2aa69a..a95c051f 100644 --- a/src/Kook.Net.Rest/API/Rest/BeginActivityParams.cs +++ b/src/Kook.Net.Rest/API/Rest/BeginActivityParams.cs @@ -5,11 +5,8 @@ namespace Kook.API.Rest; internal class BeginActivityParams { - public BeginActivityParams(ActivityType activityType) => ActivityType = activityType; - - [JsonInclude] [JsonPropertyName("data_type")] - public ActivityType ActivityType { get; private set; } + public required ActivityType ActivityType { get; set; } // Game [JsonPropertyName("id")] @@ -21,8 +18,8 @@ internal class BeginActivityParams public MusicProvider MusicProvider { get; set; } [JsonPropertyName("singer")] - public string Signer { get; set; } + public string? Signer { get; set; } [JsonPropertyName("music_name")] - public string MusicName { get; set; } + public string? MusicName { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/BlockUserParams.cs b/src/Kook.Net.Rest/API/Rest/BlockUserParams.cs index 4fe54c90..1d86e7ff 100644 --- a/src/Kook.Net.Rest/API/Rest/BlockUserParams.cs +++ b/src/Kook.Net.Rest/API/Rest/BlockUserParams.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class BlockUserParams { [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/BoostSubscription.cs b/src/Kook.Net.Rest/API/Rest/BoostSubscription.cs index d1ffcedf..d67557e8 100644 --- a/src/Kook.Net.Rest/API/Rest/BoostSubscription.cs +++ b/src/Kook.Net.Rest/API/Rest/BoostSubscription.cs @@ -20,5 +20,5 @@ internal class BoostSubscription public DateTimeOffset EndTime { get; set; } [JsonPropertyName("user")] - public User User { get; set; } + public required User User { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateAssetParams.cs b/src/Kook.Net.Rest/API/Rest/CreateAssetParams.cs index 7e4af1fb..b029d513 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateAssetParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateAssetParams.cs @@ -4,19 +4,20 @@ namespace Kook.API.Rest; internal class CreateAssetParams { - public Stream File { get; set; } - public string FileName { get; set; } + public required Stream File { get; set; } - public IReadOnlyDictionary ToDictionary() - { - Dictionary d = new() { ["file"] = new MultipartFile(File, FileName ?? GetFilename(File)) }; - return d; - } + public string? FileName { get; set; } - private static string GetFilename(Stream stream) - { - if (stream is FileStream fileStream) return Path.GetFileName(fileStream.Name); + public IReadOnlyDictionary ToDictionary() => + new Dictionary + { + ["file"] = new MultipartFile(File, FileName ?? GetFilename(File)) + }; + private static string? GetFilename(Stream stream) + { + if (stream is FileStream fileStream) + return Path.GetFileName(fileStream.Name); return null; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateAssetResponse.cs b/src/Kook.Net.Rest/API/Rest/CreateAssetResponse.cs index a05e8c41..3c274aaf 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateAssetResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateAssetResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class CreateAssetResponse { [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateDirectMessageParams.cs b/src/Kook.Net.Rest/API/Rest/CreateDirectMessageParams.cs index 2b0bbe57..e3b76659 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateDirectMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateDirectMessageParams.cs @@ -6,7 +6,7 @@ namespace Kook.API.Rest; internal class CreateDirectMessageParams { [JsonPropertyName("type")] - public MessageType Type { get; set; } + public required MessageType Type { get; set; } [JsonPropertyName("target_id")] [JsonConverter(typeof(NullableUInt64Converter))] @@ -19,7 +19,7 @@ internal class CreateDirectMessageParams public Guid? ChatCode { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("quote")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -27,19 +27,5 @@ internal class CreateDirectMessageParams [JsonPropertyName("nonce")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Nonce { get; set; } - - public CreateDirectMessageParams(MessageType messageType, ulong userId, string content) - { - Type = messageType; - UserId = userId; - Content = content; - } - - public CreateDirectMessageParams(MessageType messageType, Guid chatCode, string content) - { - Type = messageType; - ChatCode = chatCode; - Content = content; - } + public string? Nonce { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateDirectMessageResponse.cs b/src/Kook.Net.Rest/API/Rest/CreateDirectMessageResponse.cs index 404a05e6..59e22420 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateDirectMessageResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateDirectMessageResponse.cs @@ -13,5 +13,5 @@ internal class CreateDirectMessageResponse public DateTimeOffset MessageTimestamp { get; set; } [JsonPropertyName("nonce")] - public string Nonce { get; set; } + public string? Nonce { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateGameParams.cs b/src/Kook.Net.Rest/API/Rest/CreateGameParams.cs index 5a203ebe..284f4722 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGameParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGameParams.cs @@ -5,13 +5,13 @@ namespace Kook.API.Rest; internal class CreateGameParams { [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("process_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string ProcessName { get; set; } + public string? ProcessName { get; set; } [JsonPropertyName("icon")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Icon { get; set; } + public string? Icon { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/Kook.Net.Rest/API/Rest/CreateGuildBanParams.cs index 7f4a1885..03094788 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGuildBanParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -5,13 +5,13 @@ namespace Kook.API.Rest; internal class CreateGuildBanParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("target_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } [JsonPropertyName("remark")] - public string Reason { get; set; } + public string? Reason { get; set; } [JsonPropertyName("del_msg_days")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/Kook.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Kook.Net.Rest/API/Rest/CreateGuildChannelParams.cs index 6a381abc..552f6a6d 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -6,16 +6,16 @@ namespace Kook.API.Rest; internal class CreateGuildChannelParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } [JsonPropertyName("parent_id")] [JsonConverter(typeof(NullableUInt64Converter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ulong? CategoryId { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } - [JsonPropertyName("type")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChannelType? Type { get; set; } diff --git a/src/Kook.Net.Rest/API/Rest/CreateGuildEmoteParams.cs b/src/Kook.Net.Rest/API/Rest/CreateGuildEmoteParams.cs index 9f9e6deb..e759e432 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGuildEmoteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGuildEmoteParams.cs @@ -4,22 +4,24 @@ namespace Kook.API.Rest; internal class CreateGuildEmoteParams { - public string Name { get; set; } - public ulong GuildId { get; set; } - public Stream File { get; set; } + public string? Name { get; set; } + public required ulong GuildId { get; set; } + public required Stream File { get; set; } public IReadOnlyDictionary ToDictionary() { - Dictionary d = new() { ["guild_id"] = GuildId }; - - if (Name is not null) d["name"] = $"{Name}"; - - string contentType = "image/png"; - - if (File is FileStream fileStream) contentType = $"image/{Path.GetExtension(fileStream.Name)}"; - - d["emoji"] = new MultipartFile(File, Name ?? "image", contentType.Replace(".", "")); - - return d; + Dictionary dic = new() + { + ["guild_id"] = GuildId + }; + if (Name is not null) + dic["name"] = $"{Name}"; + + string contentType = File is FileStream fileStream + ? $"image/{Path.GetExtension(fileStream.Name)}" + : "image/png"; + dic["emoji"] = new MultipartFile(File, Name ?? "image", contentType.Replace(".", "")); + + return dic; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateGuildInviteResponse.cs b/src/Kook.Net.Rest/API/Rest/CreateGuildInviteResponse.cs index be86e71b..f014ef61 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGuildInviteResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGuildInviteResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class CreateGuildInviteResponse { [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateGuildRoleParams.cs b/src/Kook.Net.Rest/API/Rest/CreateGuildRoleParams.cs index efec3290..72b29ede 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateGuildRoleParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateGuildRoleParams.cs @@ -6,8 +6,8 @@ internal class CreateGuildRoleParams { [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Kook.Net.Rest/API/Rest/CreateMessageParams.cs index 464001bf..a82cf3d5 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateMessageParams.cs @@ -6,13 +6,13 @@ namespace Kook.API.Rest; internal class CreateMessageParams { [JsonPropertyName("type")] - public MessageType Type { get; set; } + public required MessageType Type { get; set; } [JsonPropertyName("target_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("quote")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -20,17 +20,10 @@ internal class CreateMessageParams [JsonPropertyName("nonce")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Nonce { get; set; } + public string? Nonce { get; set; } [JsonPropertyName("temp_target_id")] [JsonConverter(typeof(NullableUInt64Converter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ulong? EphemeralUserId { get; set; } - - public CreateMessageParams(MessageType messageType, ulong channelId, string content) - { - Type = messageType; - ChannelId = channelId; - Content = content; - } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateMessageResponse.cs b/src/Kook.Net.Rest/API/Rest/CreateMessageResponse.cs index fc177603..99710e89 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateMessageResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateMessageResponse.cs @@ -13,5 +13,5 @@ internal class CreateMessageResponse public DateTimeOffset MessageTimestamp { get; set; } [JsonPropertyName("nonce")] - public string Nonce { get; set; } + public string? Nonce { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateOrModifyChannelPermissionOverwriteResponse.cs b/src/Kook.Net.Rest/API/Rest/CreateOrModifyChannelPermissionOverwriteResponse.cs index 5752c11d..e01f6c52 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateOrModifyChannelPermissionOverwriteResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateOrModifyChannelPermissionOverwriteResponse.cs @@ -10,20 +10,15 @@ internal class CreateOrModifyChannelPermissionOverwriteResponse [JsonPropertyName("user")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public User User { get; set; } + public User? User { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public PermissionOverwriteTargetType TargetType + public PermissionOverwriteTargetType TargetType => this switch { - get - { - if (RoleId is not null) return PermissionOverwriteTargetType.Role; - - if (User is not null) return PermissionOverwriteTargetType.User; - - return PermissionOverwriteTargetType.Unspecified; - } - } + { User: not null } => PermissionOverwriteTargetType.Role, + { RoleId: not null } => PermissionOverwriteTargetType.Role, + _ => PermissionOverwriteTargetType.Unspecified, + }; [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public ulong TargetId => TargetType switch diff --git a/src/Kook.Net.Rest/API/Rest/CreateOrRemoveChannelPermissionOverwriteParams.cs b/src/Kook.Net.Rest/API/Rest/CreateOrRemoveChannelPermissionOverwriteParams.cs index d2a3a628..63c06cfc 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateOrRemoveChannelPermissionOverwriteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateOrRemoveChannelPermissionOverwriteParams.cs @@ -6,19 +6,12 @@ namespace Kook.API.Rest; internal class CreateOrRemoveChannelPermissionOverwriteParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("type")] [JsonConverter(typeof(PermissionOverwriteTargetTypeConverter))] - public PermissionOverwriteTargetType TargetType { get; set; } + public required PermissionOverwriteTargetType TargetType { get; set; } [JsonPropertyName("value")] - public ulong TargetId { get; set; } - - public CreateOrRemoveChannelPermissionOverwriteParams(ulong channelId, PermissionOverwriteTargetType targetType, ulong targetId) - { - ChannelId = channelId; - TargetType = targetType; - TargetId = targetId; - } + public required ulong TargetId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateOrRemoveGuildMuteDeafParams.cs b/src/Kook.Net.Rest/API/Rest/CreateOrRemoveGuildMuteDeafParams.cs index 3b20f7ad..a5d6cc67 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateOrRemoveGuildMuteDeafParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateOrRemoveGuildMuteDeafParams.cs @@ -6,11 +6,11 @@ internal class CreateOrRemoveGuildMuteDeafParams { [JsonPropertyName("guild_id")] [JsonNumberHandling(JsonNumberHandling.WriteAsString)] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("target_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } [JsonPropertyName("type")] - public MuteOrDeafType Type { get; set; } + public required MuteOrDeafType Type { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/CreateUserChatParams.cs b/src/Kook.Net.Rest/API/Rest/CreateUserChatParams.cs index e44ef43e..1b0e8103 100644 --- a/src/Kook.Net.Rest/API/Rest/CreateUserChatParams.cs +++ b/src/Kook.Net.Rest/API/Rest/CreateUserChatParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class CreateUserChatParams { [JsonPropertyName("target_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } public static implicit operator CreateUserChatParams(ulong userId) => new() { UserId = userId }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteDirectMessageParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteDirectMessageParams.cs index 092918d3..3dcd50c9 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteDirectMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteDirectMessageParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteDirectMessageParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } public static implicit operator DeleteDirectMessageParams(Guid messageId) => new() { MessageId = messageId }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteGameParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteGameParams.cs index 5f360b2f..36ad538c 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteGameParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteGameParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteGameParams { [JsonPropertyName("id")] - public int Id { get; set; } + public required int Id { get; set; } public static implicit operator DeleteGameParams(int id) => new() { Id = id }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteGuildChannelParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteGuildChannelParams.cs index a40c9f97..d1c1718f 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteGuildChannelParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteGuildChannelParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteGuildChannelParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } public static implicit operator DeleteGuildChannelParams(ulong channelId) => new() { ChannelId = channelId }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteGuildEmoteParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteGuildEmoteParams.cs index c7ab48dd..adfe295e 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteGuildEmoteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteGuildEmoteParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteGuildEmoteParams { [JsonPropertyName("id")] - public string Id { get; set; } + public required string Id { get; set; } public static implicit operator DeleteGuildEmoteParams(string id) => new() { Id = id }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteGuildInviteParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteGuildInviteParams.cs index d4b760b7..26c7594b 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteGuildInviteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteGuildInviteParams.cs @@ -6,7 +6,7 @@ namespace Kook.API.Rest; internal class DeleteGuildInviteParams { [JsonPropertyName("url_code")] - public string UrlCode { get; set; } + public required string UrlCode { get; set; } [JsonPropertyName("guild_id")] [JsonConverter(typeof(NullableUInt64Converter))] diff --git a/src/Kook.Net.Rest/API/Rest/DeleteGuildRoleParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteGuildRoleParams.cs index 6396ec6e..747d6ba2 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteGuildRoleParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteGuildRoleParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class DeleteGuildRoleParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("role_id")] - public uint Id { get; set; } + public required uint Id { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteMessageParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteMessageParams.cs index 35a5572c..e6e7c198 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteMessageParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class DeleteMessageParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } public static implicit operator DeleteMessageParams(Guid messageId) => new() { MessageId = messageId }; } diff --git a/src/Kook.Net.Rest/API/Rest/DeleteUserChatParams.cs b/src/Kook.Net.Rest/API/Rest/DeleteUserChatParams.cs index dc4c2c77..ef1b0a23 100644 --- a/src/Kook.Net.Rest/API/Rest/DeleteUserChatParams.cs +++ b/src/Kook.Net.Rest/API/Rest/DeleteUserChatParams.cs @@ -7,7 +7,7 @@ internal class DeleteUserChatParams { [JsonPropertyName("chat_code")] [JsonConverter(typeof(ChatCodeConverter))] - public Guid ChatCode { get; set; } + public required Guid ChatCode { get; set; } public static implicit operator DeleteUserChatParams(Guid chatCode) => new() { ChatCode = chatCode }; } diff --git a/src/Kook.Net.Rest/API/Rest/EndGameActivityParams.cs b/src/Kook.Net.Rest/API/Rest/EndGameActivityParams.cs index b9b0f076..78ebcd43 100644 --- a/src/Kook.Net.Rest/API/Rest/EndGameActivityParams.cs +++ b/src/Kook.Net.Rest/API/Rest/EndGameActivityParams.cs @@ -4,9 +4,6 @@ namespace Kook.API.Rest; internal class EndGameActivityParams { - public EndGameActivityParams(ActivityType activityType) => ActivityType = activityType; - - [JsonInclude] [JsonPropertyName("data_type")] - public ActivityType ActivityType { get; private set; } + public required ActivityType ActivityType { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/ExtendedGuild.cs b/src/Kook.Net.Rest/API/Rest/ExtendedGuild.cs index 8841632b..a6452995 100644 --- a/src/Kook.Net.Rest/API/Rest/ExtendedGuild.cs +++ b/src/Kook.Net.Rest/API/Rest/ExtendedGuild.cs @@ -7,7 +7,7 @@ internal class ExtendedGuild : Guild { [JsonPropertyName("features")] [JsonConverter(typeof(GuildFeaturesConverter))] - public GuildFeatures Features { get; set; } + public required GuildFeatures Features { get; set; } [JsonPropertyName("boost_num")] public int BoostSubscriptionCount { get; set; } @@ -22,8 +22,8 @@ internal class ExtendedGuild : Guild public int Status { get; set; } [JsonPropertyName("auto_delete_time")] - public string AutoDeleteTime { get; set; } + public string? AutoDeleteTime { get; set; } [JsonPropertyName("recommend_info")] - public RecommendInfo RecommendInfo { get; set; } + public RecommendInfo? RecommendInfo { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/FriendState.cs b/src/Kook.Net.Rest/API/Rest/FriendState.cs index 652691ff..9a938555 100644 --- a/src/Kook.Net.Rest/API/Rest/FriendState.cs +++ b/src/Kook.Net.Rest/API/Rest/FriendState.cs @@ -13,7 +13,7 @@ internal class FriendState private Kook.FriendState State { get; set; } [JsonPropertyName("friend_info")] - public User User { get; set; } + public required User User { get; set; } [JsonPropertyName("own")] public bool Own { get; set; } diff --git a/src/Kook.Net.Rest/API/Rest/GetBotGatewayResponse.cs b/src/Kook.Net.Rest/API/Rest/GetBotGatewayResponse.cs index e6603efc..825b5942 100644 --- a/src/Kook.Net.Rest/API/Rest/GetBotGatewayResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/GetBotGatewayResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class GetBotGatewayResponse { [JsonPropertyName("url")] - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/GetChannelPermissionOverwritesResponse.cs b/src/Kook.Net.Rest/API/Rest/GetChannelPermissionOverwritesResponse.cs index 907eeddd..33f395ed 100644 --- a/src/Kook.Net.Rest/API/Rest/GetChannelPermissionOverwritesResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/GetChannelPermissionOverwritesResponse.cs @@ -6,10 +6,10 @@ namespace Kook.API.Rest; internal class GetChannelPermissionOverwritesResponse { [JsonPropertyName("permission_overwrites")] - public RolePermissionOverwrite[] RolePermissionOverwrites { get; set; } + public required RolePermissionOverwrite[] RolePermissionOverwrites { get; set; } [JsonPropertyName("permission_users")] - public UserPermissionOverwrite[] UserPermissionOverwrites { get; set; } + public required UserPermissionOverwrite[] UserPermissionOverwrites { get; set; } [JsonPropertyName("permission_sync")] [JsonConverter(typeof(NumberBooleanConverter))] diff --git a/src/Kook.Net.Rest/API/Rest/GetFriendStatesResponse.cs b/src/Kook.Net.Rest/API/Rest/GetFriendStatesResponse.cs index 5fdb567b..55a0dd2b 100644 --- a/src/Kook.Net.Rest/API/Rest/GetFriendStatesResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/GetFriendStatesResponse.cs @@ -5,11 +5,11 @@ namespace Kook.API.Rest; internal class GetFriendStatesResponse { [JsonPropertyName("request")] - public FriendState[] FriendRequests { get; set; } + public required FriendState[] FriendRequests { get; set; } [JsonPropertyName("friend")] - public FriendState[] Friends { get; set; } + public required FriendState[] Friends { get; set; } [JsonPropertyName("blocked")] - public FriendState[] BlockedUsers { get; set; } + public required FriendState[] BlockedUsers { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/GetGuildMuteDeafListResponse.cs b/src/Kook.Net.Rest/API/Rest/GetGuildMuteDeafListResponse.cs index 6174a067..3b8b0e77 100644 --- a/src/Kook.Net.Rest/API/Rest/GetGuildMuteDeafListResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/GetGuildMuteDeafListResponse.cs @@ -5,10 +5,10 @@ namespace Kook.API.Rest; internal class GetGuildMuteDeafListResponse { [JsonPropertyName("mic")] - public MuteOrDeafDetail Muted { get; set; } + public required MuteOrDeafDetail Muted { get; set; } [JsonPropertyName("headset")] - public MuteOrDeafDetail Deafened { get; set; } + public required MuteOrDeafDetail Deafened { get; set; } } internal class MuteOrDeafDetail @@ -17,5 +17,5 @@ internal class MuteOrDeafDetail public MuteOrDeafType Type { get; set; } [JsonPropertyName("user_ids")] - public ulong[] UserIds { get; set; } + public required ulong[] UserIds { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/GetVoiceGatewayResponse.cs b/src/Kook.Net.Rest/API/Rest/GetVoiceGatewayResponse.cs index 5f284dbd..10ee4bc3 100644 --- a/src/Kook.Net.Rest/API/Rest/GetVoiceGatewayResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/GetVoiceGatewayResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class GetVoiceGatewayResponse { [JsonPropertyName("gateway_url")] - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/GuildMember.cs b/src/Kook.Net.Rest/API/Rest/GuildMember.cs index 32bf4ca8..11d498ef 100644 --- a/src/Kook.Net.Rest/API/Rest/GuildMember.cs +++ b/src/Kook.Net.Rest/API/Rest/GuildMember.cs @@ -6,28 +6,28 @@ namespace Kook.API.Rest; internal class GuildMember : User { [JsonPropertyName("nickname")] - public string Nickname { get; set; } + public string? Nickname { get; set; } [JsonPropertyName("mobile_verified")] - public bool MobileVerified { get; set; } + public bool? MobileVerified { get; set; } [JsonPropertyName("joined_at")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset JoinedAt { get; set; } + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset? JoinedAt { get; set; } [JsonPropertyName("active_time")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset ActiveAt { get; set; } + [JsonConverter(typeof(NullableDateTimeOffsetUnixTimeMillisecondsConverter))] + public DateTimeOffset? ActiveAt { get; set; } [JsonPropertyName("hoist_info")] - public HoistInfo HoistInfo { get; set; } + public HoistInfo? HoistInfo { get; set; } [JsonPropertyName("color")] [JsonConverter(typeof(RawValueColorConverter))] - public Color Color { get; set; } + public Color? Color { get; set; } [JsonPropertyName("roles")] - public uint[] Roles { get; set; } + public uint[]? Roles { get; set; } [JsonPropertyName("is_master")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -35,10 +35,10 @@ internal class GuildMember : User [JsonPropertyName("desc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Description { get; set; } + public string? Description { get; set; } [JsonPropertyName("abbr")] - public string Abbreviation { get; set; } + public string? Abbreviation { get; set; } } internal class HoistInfo @@ -47,7 +47,7 @@ internal class HoistInfo public uint RoleId { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("color")] public uint Color { get; set; } diff --git a/src/Kook.Net.Rest/API/Rest/HandleFriendRequestParams.cs b/src/Kook.Net.Rest/API/Rest/HandleFriendRequestParams.cs index 151aa036..1caa5827 100644 --- a/src/Kook.Net.Rest/API/Rest/HandleFriendRequestParams.cs +++ b/src/Kook.Net.Rest/API/Rest/HandleFriendRequestParams.cs @@ -6,9 +6,9 @@ namespace Kook.API.Rest; internal class HandleFriendRequestParams { [JsonPropertyName("id")] - public ulong Id { get; set; } + public required ulong Id { get; set; } [JsonPropertyName("accept")] [JsonConverter(typeof(NumberBooleanConverter))] - public bool HandleResult { get; set; } + public required bool HandleResult { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/KickOutGuildMemberParams.cs b/src/Kook.Net.Rest/API/Rest/KickOutGuildMemberParams.cs index 8aff219d..cba51cf4 100644 --- a/src/Kook.Net.Rest/API/Rest/KickOutGuildMemberParams.cs +++ b/src/Kook.Net.Rest/API/Rest/KickOutGuildMemberParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class KickOutGuildMemberParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("target_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/LeaveGuildParams.cs b/src/Kook.Net.Rest/API/Rest/LeaveGuildParams.cs index acf95ee2..22c2d9b7 100644 --- a/src/Kook.Net.Rest/API/Rest/LeaveGuildParams.cs +++ b/src/Kook.Net.Rest/API/Rest/LeaveGuildParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class LeaveGuildParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } public static implicit operator LeaveGuildParams(ulong guildId) => new() { GuildId = guildId }; } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyChannelPermissionOverwriteParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyChannelPermissionOverwriteParams.cs index 596ef13e..a3715e42 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyChannelPermissionOverwriteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyChannelPermissionOverwriteParams.cs @@ -6,14 +6,14 @@ namespace Kook.API.Rest; internal class ModifyChannelPermissionOverwriteParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("type")] [JsonConverter(typeof(PermissionOverwriteTargetTypeConverter))] public PermissionOverwriteTargetType TargetType { get; set; } [JsonPropertyName("value")] - public ulong TargetId { get; set; } + public required ulong TargetId { get; set; } [JsonPropertyName("allow")] [JsonConverter(typeof(NullableUInt64Converter))] @@ -24,14 +24,4 @@ internal class ModifyChannelPermissionOverwriteParams [JsonConverter(typeof(NullableUInt64Converter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ulong? Deny { get; set; } - - public ModifyChannelPermissionOverwriteParams(ulong channelId, PermissionOverwriteTargetType targetType, ulong targetId, ulong? allow, - ulong? deny) - { - ChannelId = channelId; - TargetType = targetType; - TargetId = targetId; - Allow = allow; - Deny = deny; - } } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyDirectMessageParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyDirectMessageParams.cs index 5b81f5d1..6f2e38e6 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyDirectMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyDirectMessageParams.cs @@ -6,19 +6,13 @@ namespace Kook.API.Rest; internal class ModifyDirectMessageParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("quote")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonConverter(typeof(NullableGuidConverter))] public Guid? QuotedMessageId { get; set; } - - public ModifyDirectMessageParams(Guid messageId, string content) - { - MessageId = messageId; - Content = content; - } } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyGameParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyGameParams.cs index 16c07ec7..cbb57ad8 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyGameParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyGameParams.cs @@ -5,13 +5,13 @@ namespace Kook.API.Rest; internal class ModifyGameParams { [JsonPropertyName("id")] - public int Id { get; set; } + public required int Id { get; set; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("icon")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Icon { get; set; } + public string? Icon { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index 2dfa95b2..61d5000e 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -6,11 +6,11 @@ namespace Kook.API.Rest; internal class ModifyGuildChannelParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("level")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/Kook.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs index de4505a3..e5ab9535 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class ModifyGuildEmoteParams { [JsonPropertyName("id")] - public string Id { get; set; } + public required string Id { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyGuildMemberNicknameParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyGuildMemberNicknameParams.cs index 9a648932..f6bb7dc5 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyGuildMemberNicknameParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyGuildMemberNicknameParams.cs @@ -6,11 +6,11 @@ namespace Kook.API.Rest; internal class ModifyGuildMemberNicknameParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("nickname")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string Nickname { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Nickname { get; set; } [JsonPropertyName("user_id")] [JsonConverter(typeof(NullableUInt64Converter))] diff --git a/src/Kook.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index 282507b3..358bdf7b 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -6,14 +6,14 @@ namespace Kook.API.Rest; internal class ModifyGuildRoleParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("role_id")] - public uint RoleId { get; set; } + public required uint RoleId { get; set; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("color")] [JsonConverter(typeof(RawValueColorConverter))] diff --git a/src/Kook.Net.Rest/API/Rest/ModifyMessageParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyMessageParams.cs index dc41a880..1abf153b 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyMessageParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -6,10 +6,10 @@ namespace Kook.API.Rest; internal class ModifyMessageParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("quote")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -20,10 +20,4 @@ internal class ModifyMessageParams [JsonConverter(typeof(NullableUInt64Converter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ulong? EphemeralUserId { get; set; } - - public ModifyMessageParams(Guid messageId, string content) - { - MessageId = messageId; - Content = content; - } } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyTextChannelParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyTextChannelParams.cs index 5eb6eb5c..d5b8405e 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyTextChannelParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyTextChannelParams.cs @@ -5,7 +5,7 @@ namespace Kook.API.Rest; internal class ModifyTextChannelParams : ModifyGuildChannelParams { [JsonPropertyName("topic")] - public string Topic { get; set; } + public string? Topic { get; set; } [JsonPropertyName("slow_mode")] public int? SlowModeInterval { get; set; } diff --git a/src/Kook.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs b/src/Kook.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs index 43a86797..b82e3fe5 100644 --- a/src/Kook.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Kook.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs @@ -15,7 +15,7 @@ internal class ModifyVoiceChannelParams : ModifyTextChannelParams [JsonPropertyName("password")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Password { get; set; } + public string? Password { get; set; } // 为 0 时表示同步,因此命名为 OverwriteVoiceRegion [JsonPropertyName("sync_guild_region")] @@ -25,5 +25,5 @@ internal class ModifyVoiceChannelParams : ModifyTextChannelParams [JsonPropertyName("region")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string VoiceRegion { get; set; } + public string? VoiceRegion { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/MoveUsersParams.cs b/src/Kook.Net.Rest/API/Rest/MoveUsersParams.cs index 32bdfe48..0d602b06 100644 --- a/src/Kook.Net.Rest/API/Rest/MoveUsersParams.cs +++ b/src/Kook.Net.Rest/API/Rest/MoveUsersParams.cs @@ -5,9 +5,9 @@ namespace Kook.API.Rest; internal class MoveUsersParams { [JsonPropertyName("target_id")] - public ulong ChannelId { get; set; } + public required ulong ChannelId { get; set; } [JsonPropertyName("user_ids")] [JsonNumberHandling(JsonNumberHandling.WriteAsString)] - public ulong[] UserIds { get; set; } + public required ulong[] UserIds { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/PagedResponseBase.cs b/src/Kook.Net.Rest/API/Rest/PagedResponseBase.cs index 19cdcd7b..2fb27295 100644 --- a/src/Kook.Net.Rest/API/Rest/PagedResponseBase.cs +++ b/src/Kook.Net.Rest/API/Rest/PagedResponseBase.cs @@ -6,10 +6,10 @@ namespace Kook.API.Rest; internal class PagedResponseBase { [JsonPropertyName("items")] - public TItem[] Items { get; set; } + public required TItem[] Items { get; set; } [JsonPropertyName("meta")] - public PageMeta Meta { get; set; } + public required PageMeta Meta { get; set; } [JsonPropertyName("sort")] [JsonConverter(typeof(PageSortInfoConverter))] @@ -37,11 +37,11 @@ public PageMeta(int page = 1, int pageSize = 100) [JsonPropertyName("total")] public int Total { get; set; } - public static PageMeta Default => new(1, 100); + public static PageMeta Default => new(); } internal struct PageSortInfo { - public string SortKey { get; set; } + public string? SortKey { get; set; } public SortMode SortMode { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/QueryMessagesResponse.cs b/src/Kook.Net.Rest/API/Rest/QueryMessagesResponse.cs index a2799d65..686ae2f4 100644 --- a/src/Kook.Net.Rest/API/Rest/QueryMessagesResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/QueryMessagesResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class QueryMessagesResponse { [JsonPropertyName("items")] - public Message[] Items { get; set; } + public required Message[] Items { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/QueryUserChatMessagesResponse.cs b/src/Kook.Net.Rest/API/Rest/QueryUserChatMessagesResponse.cs index aa548493..51e6bd36 100644 --- a/src/Kook.Net.Rest/API/Rest/QueryUserChatMessagesResponse.cs +++ b/src/Kook.Net.Rest/API/Rest/QueryUserChatMessagesResponse.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class QueryUserChatMessagesResponse { [JsonPropertyName("items")] - public DirectMessage[] Items { get; set; } + public required DirectMessage[] Items { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/RemoveFriendParams.cs b/src/Kook.Net.Rest/API/Rest/RemoveFriendParams.cs index d79fdbaa..897b5e48 100644 --- a/src/Kook.Net.Rest/API/Rest/RemoveFriendParams.cs +++ b/src/Kook.Net.Rest/API/Rest/RemoveFriendParams.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class RemoveFriendParams { [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/RemoveGuildBanParams.cs b/src/Kook.Net.Rest/API/Rest/RemoveGuildBanParams.cs index a34c9377..5ca3a03b 100644 --- a/src/Kook.Net.Rest/API/Rest/RemoveGuildBanParams.cs +++ b/src/Kook.Net.Rest/API/Rest/RemoveGuildBanParams.cs @@ -5,8 +5,8 @@ namespace Kook.API.Rest; internal class RemoveGuildBanParams { [JsonPropertyName("guild_id")] - public ulong GuildId { get; set; } + public required ulong GuildId { get; set; } [JsonPropertyName("target_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/RemoveReactionParams.cs b/src/Kook.Net.Rest/API/Rest/RemoveReactionParams.cs index c6d8d7e6..bfe7de89 100644 --- a/src/Kook.Net.Rest/API/Rest/RemoveReactionParams.cs +++ b/src/Kook.Net.Rest/API/Rest/RemoveReactionParams.cs @@ -6,10 +6,10 @@ namespace Kook.API.Rest; internal class RemoveReactionParams { [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } + public required Guid MessageId { get; set; } [JsonPropertyName("emoji")] - public string EmojiId { get; set; } + public required string EmojiId { get; set; } [JsonPropertyName("user_id")] [JsonConverter(typeof(NullableUInt64Converter))] diff --git a/src/Kook.Net.Rest/API/Rest/RequestFriendParams.cs b/src/Kook.Net.Rest/API/Rest/RequestFriendParams.cs index ff919336..5d1e0bda 100644 --- a/src/Kook.Net.Rest/API/Rest/RequestFriendParams.cs +++ b/src/Kook.Net.Rest/API/Rest/RequestFriendParams.cs @@ -5,12 +5,12 @@ namespace Kook.API.Rest; internal class RequestFriendParams { [JsonPropertyName("user_code")] - public string FullQualification { get; set; } + public required string FullQualification { get; set; } [JsonPropertyName("from")] - public RequestFriendSource Source { get; set; } + public required RequestFriendSource Source { get; set; } [JsonPropertyName("guild_id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public ulong GuildId { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ulong? GuildId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/RestResponseBase.cs b/src/Kook.Net.Rest/API/Rest/RestResponseBase.cs index 64a3c3e6..6217b1b4 100644 --- a/src/Kook.Net.Rest/API/Rest/RestResponseBase.cs +++ b/src/Kook.Net.Rest/API/Rest/RestResponseBase.cs @@ -8,8 +8,8 @@ internal class RestResponseBase public KookErrorCode Code { get; set; } [JsonPropertyName("message")] - public string Message { get; set; } + public required string Message { get; set; } [JsonPropertyName("data")] - public object Data { get; set; } + public required object Data { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/RichGuild.cs b/src/Kook.Net.Rest/API/Rest/RichGuild.cs index 4ad2a198..5565012b 100644 --- a/src/Kook.Net.Rest/API/Rest/RichGuild.cs +++ b/src/Kook.Net.Rest/API/Rest/RichGuild.cs @@ -5,14 +5,14 @@ namespace Kook.API.Rest; internal class RichGuild : ExtendedGuild { [JsonPropertyName("emojis")] - public Emoji[] Emojis { get; set; } + public required Emoji[] Emojis { get; set; } [JsonPropertyName("banner")] - public string Banner { get; set; } + public required string Banner { get; set; } [JsonPropertyName("my_nickname")] - public string CurrentUserNickname { get; set; } + public string? CurrentUserNickname { get; set; } [JsonPropertyName("my_roles")] - public uint[] CurrentUserRoles { get; set; } + public uint[]? CurrentUserRoles { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/SelfUser.cs b/src/Kook.Net.Rest/API/Rest/SelfUser.cs index 2bb7b469..8f3b0c37 100644 --- a/src/Kook.Net.Rest/API/Rest/SelfUser.cs +++ b/src/Kook.Net.Rest/API/Rest/SelfUser.cs @@ -8,11 +8,11 @@ internal class SelfUser : User public bool MobileVerified { get; set; } [JsonPropertyName("MobilePrefix")] - public string MobilePrefix { get; set; } + public string? MobilePrefix { get; set; } [JsonPropertyName("Mobile")] - public string Mobile { get; set; } + public string? Mobile { get; set; } [JsonPropertyName("InvitedCount")] - public int? InvitedCount { get; set; } + public int InvitedCount { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/SyncChannelPermissionsParams.cs b/src/Kook.Net.Rest/API/Rest/SyncChannelPermissionsParams.cs index 04b384cb..ba1a2ddb 100644 --- a/src/Kook.Net.Rest/API/Rest/SyncChannelPermissionsParams.cs +++ b/src/Kook.Net.Rest/API/Rest/SyncChannelPermissionsParams.cs @@ -5,7 +5,5 @@ namespace Kook.API.Rest; internal class SyncChannelPermissionsParams { [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } - - public SyncChannelPermissionsParams(ulong channelId) => ChannelId = channelId; + public required ulong ChannelId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/UnblockUserParams.cs b/src/Kook.Net.Rest/API/Rest/UnblockUserParams.cs index 9bb6e4f4..7d6cb018 100644 --- a/src/Kook.Net.Rest/API/Rest/UnblockUserParams.cs +++ b/src/Kook.Net.Rest/API/Rest/UnblockUserParams.cs @@ -5,5 +5,5 @@ namespace Kook.API.Rest; internal class UnblockUserParams { [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public required ulong UserId { get; set; } } diff --git a/src/Kook.Net.Rest/API/Rest/UpdateIntimacyValueParams.cs b/src/Kook.Net.Rest/API/Rest/UpdateIntimacyValueParams.cs index 0bf14fb0..0c53eaea 100644 --- a/src/Kook.Net.Rest/API/Rest/UpdateIntimacyValueParams.cs +++ b/src/Kook.Net.Rest/API/Rest/UpdateIntimacyValueParams.cs @@ -8,11 +8,11 @@ internal class UpdateIntimacyValueParams public ulong UserId { get; set; } [JsonPropertyName("score")] - public int Score { get; set; } + public int? Score { get; set; } [JsonPropertyName("social_info")] - public string SocialInfo { get; set; } + public string? SocialInfo { get; set; } [JsonPropertyName("img_id")] - public uint ImageId { get; set; } + public uint? ImageId { get; set; } } diff --git a/src/Kook.Net.Rest/BaseKookClient.cs b/src/Kook.Net.Rest/BaseKookClient.cs index d8c01427..0d418bd6 100644 --- a/src/Kook.Net.Rest/BaseKookClient.cs +++ b/src/Kook.Net.Rest/BaseKookClient.cs @@ -75,7 +75,7 @@ public event Func SentRequest /// /// Gets the logged-in user. /// - public ISelfUser CurrentUser { get; protected set; } + public ISelfUser? CurrentUser { get; protected set; } /// public TokenType TokenType => ApiClient.AuthTokenType; @@ -97,15 +97,14 @@ internal BaseKookClient(KookRestConfig config, API.KookRestApiClient client) ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}") - .ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}") - .ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - ApiClient.SentRequest += (method, endpoint, millis) => _sentRequest.InvokeAsync(method, endpoint, millis); + ApiClient.SentRequest += (method, endpoint, millis) => + _sentRequest.InvokeAsync(method, endpoint, millis); } internal virtual void Dispose(bool disposing) @@ -148,7 +147,8 @@ internal virtual async Task LoginInternalAsync(TokenType tokenType, string token await LogManager.WriteInitialLog().ConfigureAwait(false); } - if (LoginState != LoginState.LoggedOut) await LogoutInternalAsync().ConfigureAwait(false); + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); LoginState = LoginState.LoggingIn; @@ -157,6 +157,7 @@ internal virtual async Task LoginInternalAsync(TokenType tokenType, string token // If token validation is enabled, validate the token and let it throw any ArgumentExceptions // that result from invalid parameters if (validateToken) + { try { TokenUtils.ValidateToken(tokenType, token); @@ -166,6 +167,7 @@ internal virtual async Task LoginInternalAsync(TokenType tokenType, string token // log these ArgumentExceptions and allow for the client to attempt to log in anyways await LogManager.WarningAsync("Kook", "A supplied token was invalid.", ex).ConfigureAwait(false); } + } await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); await OnLoginAsync(tokenType, token).ConfigureAwait(false); @@ -180,8 +182,7 @@ internal virtual async Task LoginInternalAsync(TokenType tokenType, string token await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLoginAsync(TokenType tokenType, string token) - => Task.Delay(0); + internal virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask; /// /// Logs out from the Kook API. @@ -202,22 +203,17 @@ public async Task LogoutAsync() internal virtual async Task LogoutInternalAsync() { await ApiClient.GoOfflineAsync(); - - if (LoginState == LoginState.LoggedOut) return; - + if (LoginState == LoginState.LoggedOut) + return; LoginState = LoginState.LoggingOut; - await ApiClient.LogoutAsync().ConfigureAwait(false); - await OnLogoutAsync().ConfigureAwait(false); CurrentUser = null; LoginState = LoginState.LoggedOut; - await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLogoutAsync() - => Task.Delay(0); + internal virtual Task OnLogoutAsync() => Task.CompletedTask; /// public virtual ConnectionState ConnectionState => ConnectionState.Disconnected; @@ -227,55 +223,55 @@ internal virtual Task OnLogoutAsync() #region IKookClient /// - ISelfUser IKookClient.CurrentUser => CurrentUser; + ISelfUser? IKookClient.CurrentUser => CurrentUser; /// - Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); + Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); /// - Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(ImmutableArray.Create()); + Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(ImmutableArray.Create()); /// - Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); + Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); /// - Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions options) - => Task.FromResult(null); + Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); /// - Task> IKookClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(ImmutableArray.Create()); + Task> IKookClient.GetDMChannelsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(ImmutableArray.Create()); /// - Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); + Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); /// - Task IKookClient.GetUserAsync(string username, string identifyNumber, RequestOptions options) - => Task.FromResult(null); + Task IKookClient.GetUserAsync(string username, string identifyNumber, RequestOptions? options) => + Task.FromResult(null); /// - Task> IKookClient.GetFriendsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(ImmutableArray.Create()); + Task> IKookClient.GetFriendsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(ImmutableArray.Create()); /// - Task> IKookClient.GetFriendRequestsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(ImmutableArray.Create()); + Task> IKookClient.GetFriendRequestsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(ImmutableArray.Create()); /// - Task> IKookClient.GetBlockedUsersAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(ImmutableArray.Create()); + Task> IKookClient.GetBlockedUsersAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(ImmutableArray.Create()); /// - Task IKookClient.StartAsync() - => Task.Delay(0); + Task IKookClient.StartAsync() => + Task.Delay(0); /// - Task IKookClient.StopAsync() - => Task.Delay(0); + Task IKookClient.StopAsync() => + Task.Delay(0); #endregion } diff --git a/src/Kook.Net.Rest/ClientHelper.cs b/src/Kook.Net.Rest/ClientHelper.cs index 02c5ebed..72dd9303 100644 --- a/src/Kook.Net.Rest/ClientHelper.cs +++ b/src/Kook.Net.Rest/ClientHelper.cs @@ -6,151 +6,144 @@ namespace Kook.Rest; internal static class ClientHelper { - public static async Task GetGuildAsync(BaseKookClient client, - ulong id, RequestOptions options) + public static async Task GetGuildAsync(BaseKookClient client, ulong id, RequestOptions? options) { ExtendedGuild model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false); - if (model != null) return RestGuild.Create(client, model); - - return null; + return RestGuild.Create(client, model); } - public static async Task> GetGuildsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetGuildsAsync(BaseKookClient client, RequestOptions? options) { ImmutableArray.Builder guilds = ImmutableArray.CreateBuilder(); if (client.TokenType is TokenType.Bot) { IReadOnlyCollection models = await client.ApiClient.ListGuildsAsync(options).ConfigureAwait(false); - foreach (RichGuild model in models) guilds.Add(RestGuild.Create(client, model)); + foreach (RichGuild model in models) + guilds.Add(RestGuild.Create(client, model)); } else { IEnumerable models = await client.ApiClient.GetGuildsAsync(options: options).FlattenAsync().ConfigureAwait(false); - foreach (Guild model in models) guilds.Add(RestGuild.Create(client, model)); + foreach (Guild model in models) + guilds.Add(RestGuild.Create(client, model)); } return guilds.ToImmutable(); } - public static async Task GetChannelAsync(BaseKookClient client, - ulong id, RequestOptions options) + public static async Task GetChannelAsync(BaseKookClient client, ulong id, RequestOptions? options) { Channel model = await client.ApiClient.GetGuildChannelAsync(id, options).ConfigureAwait(false); - if (model != null) return RestChannel.Create(client, model); - - return null; + return RestChannel.Create(client, model); } - public static async Task GetDMChannelAsync(BaseKookClient client, - Guid chatCode, RequestOptions options) + public static async Task GetDMChannelAsync(BaseKookClient client, Guid chatCode, RequestOptions? options) { UserChat model = await client.ApiClient.GetUserChatAsync(chatCode, options).ConfigureAwait(false); - if (model != null) return RestDMChannel.Create(client, model); - - return null; + return RestDMChannel.Create(client, model); } - public static async Task> GetDMChannelsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetDMChannelsAsync(BaseKookClient client, RequestOptions? options) { IEnumerable model = await client.ApiClient.GetUserChatsAsync(options: options).FlattenAsync().ConfigureAwait(false); - if (model != null) return model.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); - - return null; + return model?.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray() ?? []; } public static async Task GetUserAsync(BaseKookClient client, - ulong id, RequestOptions options) + ulong id, RequestOptions? options) { User model = await client.ApiClient.GetUserAsync(id, options).ConfigureAwait(false); - if (model != null) return RestUser.Create(client, model); - - return null; + return RestUser.Create(client, model); } - public static async Task GetGuildMemberAsync(BaseKookClient client, - ulong guildId, ulong id, RequestOptions options) + public static async Task GetGuildMemberAsync(BaseKookClient client, + ulong guildId, ulong id, RequestOptions? options) { RestGuild guild = await GetGuildAsync(client, guildId, options).ConfigureAwait(false); - if (guild == null) return null; - GuildMember model = await client.ApiClient.GetGuildMemberAsync(guildId, id, options).ConfigureAwait(false); - if (model != null) return RestGuildUser.Create(client, guild, model); - - return null; + return RestGuildUser.Create(client, guild, model); } public static async Task MoveUsersAsync(BaseKookClient client, IEnumerable userIds, IVoiceChannel targetChannel, - RequestOptions options) + RequestOptions? options) { - MoveUsersParams args = new() { ChannelId = targetChannel.Id, UserIds = userIds.Select(x => x.Id).ToArray() }; + MoveUsersParams args = new() + { + ChannelId = targetChannel.Id, + UserIds = userIds.Select(x => x.Id).ToArray() + }; await client.ApiClient.MoveUsersAsync(args, options).ConfigureAwait(false); } - public static async Task> GetFriendsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetFriendsAsync(BaseKookClient client, RequestOptions? options) { GetFriendStatesResponse models = await client.ApiClient.GetFriendStatesAsync(FriendState.Accepted, options).ConfigureAwait(false); - if (models != null) return models.Friends.Select(x => RestUser.Create(client, x.User)).ToImmutableArray(); - - return null; + return [..models.Friends.Select(x => RestUser.Create(client, x.User))]; } - // public static async Task RequestFriendAsync(BaseKookClient client, string username, ushort identifyNumberValue, RequestOptions options) - // { - // // TODO: Add a better way to validate the identify number. - // Preconditions.AtMost(identifyNumberValue, (ushort)9999, nameof(identifyNumberValue)); - // Preconditions.AtLeast(identifyNumberValue, (ushort)1, nameof(identifyNumberValue)); - // RequestFriendParams args = new() { FullQualification = $"{username}#{identifyNumberValue:D4}", Source = RequestFriendSource.FullQualification }; - // await client.ApiClient.RequestFriendAsync(args, options).ConfigureAwait(false); - // } - - public static async Task> GetFriendRequestsAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetFriendRequestsAsync(BaseKookClient client, RequestOptions? options) { GetFriendStatesResponse models = await client.ApiClient.GetFriendStatesAsync(FriendState.Pending, options).ConfigureAwait(false); - if (models != null) return models.FriendRequests.Select(x => RestFriendRequest.Create(client, x)).ToImmutableArray(); - - return null; + return [..models.FriendRequests.Select(x => RestFriendRequest.Create(client, x))]; } - public static async Task> GetBlockedUsersAsync(BaseKookClient client, RequestOptions options) + public static async Task> GetBlockedUsersAsync(BaseKookClient client, RequestOptions? options) { - GetFriendStatesResponse models = await client.ApiClient.GetFriendStatesAsync(FriendState.Blocked, options).ConfigureAwait(false); - if (models != null) return models.BlockedUsers.Select(x => RestUser.Create(client, x.User)).ToImmutableArray(); - - return null; + GetFriendStatesResponse? models = await client.ApiClient.GetFriendStatesAsync(FriendState.Blocked, options).ConfigureAwait(false); + return models?.BlockedUsers.Select(x => RestUser.Create(client, x.User)).ToImmutableArray() ?? []; } - public static async Task CreateAssetAsync(BaseKookClient client, Stream stream, string fileName, RequestOptions options) + public static async Task CreateAssetAsync(BaseKookClient client, Stream stream, string filename, RequestOptions? options) { - CreateAssetResponse model = await client.ApiClient.CreateAssetAsync(new CreateAssetParams { File = stream, FileName = fileName }, options); - if (model != null) return model.Url; - - return null; + CreateAssetParams args = new() + { + File = stream, + FileName = filename + }; + CreateAssetResponse model = await client.ApiClient.CreateAssetAsync(args, options); + return model.Url; } - public static IAsyncEnumerable> GetGamesAsync(BaseKookClient client, GameCreationSource? source, - RequestOptions options) => - client.ApiClient.GetGamesAsync(source, options: options) + public static IAsyncEnumerable> GetGamesAsync(BaseKookClient client, GameCreationSource? source, RequestOptions? options) + { + return client.ApiClient.GetGamesAsync(source, options: options) .Select(x => x.Select(y => RestGame.Create(client, y)).ToImmutableArray() as IReadOnlyCollection); + } - public static async Task CreateGameAsync(BaseKookClient client, string name, string processName, string iconUrl, RequestOptions options) + public static async Task CreateGameAsync(BaseKookClient client, string name, string? processName, string? iconUrl, RequestOptions? options = null) { - CreateGameParams args = new() { Icon = iconUrl, Name = name, ProcessName = processName }; + CreateGameParams args = new() + { + Icon = iconUrl, + Name = name, + ProcessName = processName + }; Game model = await client.ApiClient.CreateGameAsync(args, options).ConfigureAwait(false); return RestGame.Create(client, model); } - public static async Task DeleteGameAsync(BaseKookClient client, int id, RequestOptions options) => + public static async Task DeleteGameAsync(BaseKookClient client, int id, RequestOptions? options) => await client.ApiClient.DeleteGameAsync(id, options).ConfigureAwait(false); - public static async Task AddRoleAsync(BaseKookClient client, ulong guildId, ulong userId, uint roleId, RequestOptions options = null) + public static async Task AddRoleAsync(BaseKookClient client, ulong guildId, ulong userId, uint roleId, RequestOptions? options = null) { - AddOrRemoveRoleParams args = new() { GuildId = guildId, RoleId = roleId, UserId = userId }; + AddOrRemoveRoleParams args = new() + { + GuildId = guildId, + RoleId = roleId, + UserId = userId + }; await client.ApiClient.AddRoleAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveRoleAsync(BaseKookClient client, ulong guildId, ulong userId, uint roleId, RequestOptions options = null) + public static async Task RemoveRoleAsync(BaseKookClient client, ulong guildId, ulong userId, uint roleId, RequestOptions? options = null) { - AddOrRemoveRoleParams args = new() { GuildId = guildId, RoleId = roleId, UserId = userId }; + AddOrRemoveRoleParams args = new() + { + GuildId = guildId, + RoleId = roleId, + UserId = userId + }; await client.ApiClient.RemoveRoleAsync(args, options).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs index f2ab46d4..3d0b7142 100644 --- a/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -10,23 +10,26 @@ internal static class ChannelHelper { #region General - public static async Task DeleteGuildChannelAsync(IGuildChannel channel, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteGuildChannelAsync(IGuildChannel channel, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteGuildChannelAsync(channel.Id, options).ConfigureAwait(false); public static async Task ModifyAsync(IGuildChannel channel, BaseKookClient client, - Action func, - RequestOptions options) + Action func, RequestOptions? options) { ModifyGuildChannelProperties args = new(); func(args); - ModifyGuildChannelParams apiArgs = new() { ChannelId = channel.Id, Name = args.Name, Position = args.Position, CategoryId = args.CategoryId }; + ModifyGuildChannelParams apiArgs = new() + { + ChannelId = channel.Id, + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId + }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ModifyAsync(ITextChannel channel, BaseKookClient client, - Action func, - RequestOptions options) + Action func, RequestOptions? options) { ModifyTextChannelProperties args = new(); func(args); @@ -43,8 +46,7 @@ public static async Task ModifyAsync(ITextChannel channel, BaseKookClient } public static async Task ModifyAsync(IVoiceChannel channel, BaseKookClient client, - Action func, - RequestOptions options) + Action func, RequestOptions? options) { ModifyVoiceChannelProperties args = new(); func(args); @@ -65,13 +67,12 @@ public static async Task ModifyAsync(IVoiceChannel channel, BaseKookClien return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task DeleteDMChannelAsync(IDMChannel channel, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteDMChannelAsync(IDMChannel channel, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteUserChatAsync(channel.ChatCode, options).ConfigureAwait(false); - public static async Task UpdateAsync(RestChannel channel, BaseKookClient client, RequestOptions options) + public static async Task UpdateAsync(RestChannel channel, BaseKookClient client, RequestOptions? options) { - Channel model = await client.ApiClient.GetGuildChannelAsync(channel.Id, options).ConfigureAwait(false); + Model model = await client.ApiClient.GetGuildChannelAsync(channel.Id, options).ConfigureAwait(false); channel.Update(model); } @@ -80,56 +81,53 @@ public static async Task UpdateAsync(RestChannel channel, BaseKookClient client, #region Messages public static async Task GetMessageAsync(IMessageChannel channel, BaseKookClient client, - Guid id, RequestOptions options) + Guid id, RequestOptions? options) { - ulong? guildId = (channel as IGuildChannel)?.GuildId; - IGuild guild = guildId != null - ? await (client as IKookClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) + IGuild? guild = channel is IGuildChannel { GuildId: var guildId } + ? await (client as IKookClient).GetGuildAsync(guildId, CacheMode.CacheOnly) : null; Message model = await client.ApiClient.GetMessageAsync(id, options).ConfigureAwait(false); - if (model == null) return null; - IUser author = await MessageHelper.GetAuthorAsync(client, guild, model.Author); return RestMessage.Create(client, channel, author, model); } - public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, - BaseKookClient client, - Guid? referenceMessageId, Direction dir, int limit, bool includeReferenceMessage, RequestOptions options) + public static IAsyncEnumerable> GetMessagesAsync( + IMessageChannel channel, BaseKookClient client, + Guid? referenceMessageId, Direction direction, int limit, bool includeReferenceMessage, + RequestOptions? options) { - ulong? guildId = (channel as IGuildChannel)?.GuildId; - IGuild guild = guildId != null - ? (client as IKookClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).GetAwaiter().GetResult() + IGuild? guild = channel is IGuildChannel { GuildId: var guildId } + ? (client as IKookClient).GetGuildAsync(guildId, CacheMode.CacheOnly).GetAwaiter().GetResult() : null; - if (dir - == Direction - .Around) // && limit > KookConfig.MaxMessagesPerBatch // Around mode returns error messages from endpoint + if (direction == Direction.Around) { int around = limit / 2; if (referenceMessageId.HasValue) { - IAsyncEnumerable> messages = GetMessagesAsync(channel, client, referenceMessageId, Direction.Before, - around, - includeReferenceMessage, options); - messages = messages.Concat(GetMessagesAsync(channel, client, referenceMessageId, Direction.After, + IAsyncEnumerable> before = GetMessagesAsync( + channel, client, referenceMessageId, Direction.Before, + around, includeReferenceMessage, options); + return before.Concat(GetMessagesAsync( + channel, client, referenceMessageId, Direction.After, around, false, options)); - return messages; } - else // Shouldn't happen since there's no public overload for Guid? and Direction - return GetMessagesAsync(channel, client, null, Direction.Before, around + 1, includeReferenceMessage, - options); + + return GetMessagesAsync(channel, client, null, Direction.Before, + around + 1, includeReferenceMessage, options); } return new PagedAsyncEnumerable( KookConfig.MaxMessagesPerBatch, - async (info, ct) => + async (info, _) => { - IReadOnlyCollection models = await client.ApiClient.QueryMessagesAsync(channel.Id, info.Position, - dir: dir, count: limit, options: options).ConfigureAwait(false); + IReadOnlyCollection models = await client.ApiClient + .QueryMessagesAsync(channel.Id, info.Position, null, direction, limit, options) + .ConfigureAwait(false); ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); + // Insert the reference message before query results - if (includeReferenceMessage && info.Position.HasValue && dir == Direction.After) + if (includeReferenceMessage && info.Position.HasValue && direction == Direction.After) { Message currentMessage = await client.ApiClient.GetMessageAsync(info.Position.Value, options); IUser currentMessageAuthor = await MessageHelper.GetAuthorAsync(client, guild, currentMessage.Author); @@ -143,7 +141,7 @@ public static IAsyncEnumerable> GetMessagesAsyn } // Append the reference message after query results - if (includeReferenceMessage && info.Position.HasValue && dir == Direction.Before) + if (includeReferenceMessage && info.Position.HasValue && direction == Direction.Before) { Message currentMessage = await client.ApiClient.GetMessageAsync(info.Position.Value, options); IUser currentMessageAuthor = await MessageHelper.GetAuthorAsync(client, guild, currentMessage.Author); @@ -154,21 +152,12 @@ public static IAsyncEnumerable> GetMessagesAsyn }, (info, lastPage) => { - if (lastPage.Count != KookConfig.MaxMessagesPerBatch) return false; + if (lastPage.Count != KookConfig.MaxMessagesPerBatch) + return false; - if (dir == Direction.Before) -#if NET6_0_OR_GREATER - info.Position = lastPage.MinBy(x => x.Timestamp)?.Id; -#else - info.Position = lastPage.OrderBy(x => x.Timestamp).FirstOrDefault()?.Id; -#endif - else - -#if NET6_0_OR_GREATER - info.Position = lastPage.MaxBy(x => x.Timestamp)?.Id; -#else - info.Position = lastPage.OrderByDescending(x => x.Timestamp).FirstOrDefault()?.Id; -#endif + info.Position = direction == Direction.Before + ? lastPage.MinBy(x => x.Timestamp)?.Id + : lastPage.MaxBy(x => x.Timestamp)?.Id; return true; }, referenceMessageId, @@ -178,21 +167,20 @@ public static IAsyncEnumerable> GetMessagesAsyn public static async Task> GetPinnedMessagesAsync(IMessageChannel channel, BaseKookClient client, - RequestOptions options) + RequestOptions? options) { - ulong? guildId = (channel as IGuildChannel)?.GuildId; - IGuild guild = guildId != null - ? await (client as IKookClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) + IGuild? guild = channel is IGuildChannel { GuildId: var guildId } + ? await (client as IKookClient).GetGuildAsync(guildId, CacheMode.CacheOnly) : null; - IReadOnlyCollection models = await client.ApiClient.QueryMessagesAsync(channel.Id, queryPin: true, options: options) - .ConfigureAwait(false); + IReadOnlyCollection models = await client.ApiClient + .QueryMessagesAsync(channel.Id, queryPin: true, options: options).ConfigureAwait(false); ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); foreach (Message model in models) { IUser author = await MessageHelper.GetAuthorAsync(client, guild, model.Author); RestMessage message = RestMessage.Create(client, channel, author, model); - if (message is RestUserMessage userMessage) userMessage.IsPinned = true; - + if (message is RestUserMessage userMessage) + userMessage.IsPinned = true; builder.Add(message); } @@ -200,12 +188,16 @@ public static async Task> GetPinnedMessagesAsyn } public static async Task> SendMessageAsync(IMessageChannel channel, - BaseKookClient client, MessageType messageType, string content, RequestOptions options, IQuote quote = null, - IUser ephemeralUser = null) + BaseKookClient client, MessageType messageType, string content, IQuote? quote, IUser? ephemeralUser, + RequestOptions? options) { - CreateMessageParams args = new(messageType, channel.Id, content) + CreateMessageParams args = new() { - QuotedMessageId = quote?.QuotedMessageId, EphemeralUserId = ephemeralUser?.Id + Type = messageType, + ChannelId = channel.Id, + Content = content, + QuotedMessageId = quote?.QuotedMessageId, + EphemeralUserId = ephemeralUser?.Id }; CreateMessageResponse model = await client.ApiClient.CreateMessageAsync(args, options).ConfigureAwait(false); return new Cacheable(null, model.MessageId, false, @@ -213,53 +205,56 @@ public static async Task> SendMessageAsync(IMessag } public static async Task> SendCardsAsync(IMessageChannel channel, - BaseKookClient client, IEnumerable cards, RequestOptions options, IQuote quote = null, - IUser ephemeralUser = null) + BaseKookClient client, IEnumerable cards, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) { string json = MessageHelper.SerializeCards(cards); - return await SendMessageAsync(channel, client, MessageType.Card, json, options, quote, - ephemeralUser); + return await SendMessageAsync(channel, client, MessageType.Card, json, quote, ephemeralUser, options); } public static Task> SendCardAsync(IMessageChannel channel, - BaseKookClient client, ICard card, RequestOptions options, IQuote quote = null, - IUser ephemeralUser = null) - => SendCardsAsync(channel, client, new[] { card }, options, quote, ephemeralUser); + BaseKookClient client, ICard card, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardsAsync(channel, client, [card], quote, ephemeralUser, options); public static async Task> SendFileAsync(IMessageChannel channel, - BaseKookClient client, string path, string fileName, AttachmentType type, RequestOptions options, - IQuote quote = null, IUser ephemeralUser = null) + BaseKookClient client, string path, string filename, AttachmentType type, IQuote? quote, IUser? ephemeralUser, + RequestOptions? options) { - using FileAttachment file = new(path, fileName, type); - return await SendFileAsync(channel, client, file, options, quote, ephemeralUser); + using FileAttachment file = new(path, filename, type); + return await SendFileAsync(channel, client, file, quote, ephemeralUser, options); } public static async Task> SendFileAsync(IMessageChannel channel, - BaseKookClient client, Stream stream, string fileName, AttachmentType type, RequestOptions options, - IQuote quote = null, IUser ephemeralUser = null) + BaseKookClient client, Stream stream, string filename, AttachmentType type, IQuote? quote, IUser? ephemeralUser, + RequestOptions? options) { - using FileAttachment file = new(stream, fileName, type); - return await SendFileAsync(channel, client, file, options, quote, ephemeralUser); + using FileAttachment file = new(stream, filename, type); + return await SendFileAsync(channel, client, file, quote, ephemeralUser, options); } public static async Task> SendFileAsync(IMessageChannel channel, - BaseKookClient client, FileAttachment attachment, RequestOptions options, - IQuote quote = null, IUser ephemeralUser = null) + BaseKookClient client, FileAttachment attachment, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) { switch (attachment.Mode) { case CreateAttachmentMode.FilePath: case CreateAttachmentMode.Stream: - if (attachment.Uri != null) - break; + { + if (attachment.Uri is not null) break; + if (attachment.Stream is null) + throw new ArgumentNullException(nameof(attachment.Stream), "The stream cannot be null."); + CreateAssetParams createAssetParams = new() + { + File = attachment.Stream, + FileName = attachment.FileName + }; CreateAssetResponse assetResponse = await client.ApiClient - .CreateAssetAsync(new CreateAssetParams { File = attachment.Stream, FileName = attachment.FileName }, options); + .CreateAssetAsync(createAssetParams, options).ConfigureAwait(false); attachment.Uri = new Uri(assetResponse.Url); + } break; case CreateAttachmentMode.AssetUri: - if (!UrlValidation.ValidateKookAssetUrl(attachment.Uri.OriginalString)) + if (attachment.Uri is null || !UrlValidation.ValidateKookAssetUrl(attachment.Uri.OriginalString)) throw new ArgumentException("The uri cannot be blank.", nameof(attachment.Uri)); - break; default: throw new ArgumentOutOfRangeException(nameof(attachment.Mode), attachment.Mode, "Unknown attachment mode"); @@ -267,97 +262,99 @@ public static async Task> SendFileAsync(IMessageCh return attachment.Type switch { - AttachmentType.File => await SendMessageAsync(channel, client, MessageType.File, - attachment.Uri.OriginalString, options, quote, ephemeralUser), - AttachmentType.Image => await SendMessageAsync(channel, client, MessageType.Image, - attachment.Uri.OriginalString, options, quote, ephemeralUser), - AttachmentType.Video => await SendMessageAsync(channel, client, MessageType.Video, - attachment.Uri.OriginalString, options, quote, ephemeralUser), - AttachmentType.Audio => await SendCardAsync(channel, client, - new CardBuilder().WithSize(CardSize.Large) - .WithTheme(CardTheme.None) - .AddModule(x => x - .WithSource(attachment.Uri.OriginalString) - .WithTitle(attachment.FileName)) - .Build(), options, quote, ephemeralUser), + AttachmentType.File => await SendAttachmentAsync(MessageType.File).ConfigureAwait(false), + AttachmentType.Image => await SendAttachmentAsync(MessageType.Image).ConfigureAwait(false), + AttachmentType.Video => await SendAttachmentAsync(MessageType.Video).ConfigureAwait(false), + AttachmentType.Audio => await SendCardAsync( + channel, client, + new CardBuilder(theme: CardTheme.None, size: CardSize.Large) + .AddModule(new AudioModuleBuilder(attachment.Uri.OriginalString, attachment.FileName)) + .Build(), + quote, ephemeralUser, options).ConfigureAwait(false), _ => throw new ArgumentOutOfRangeException(nameof(attachment.Type), attachment.Type, "Unknown attachment type") }; + + Task> SendAttachmentAsync(MessageType messageType) => + SendMessageAsync(channel, client, messageType, attachment.Uri.OriginalString, quote, ephemeralUser, options); } - public static Task DeleteMessageAsync(IMessageChannel channel, Guid messageId, BaseKookClient client, - RequestOptions options) - => MessageHelper.DeleteAsync(messageId, client, options); + public static Task DeleteMessageAsync(IMessageChannel channel, + Guid messageId, BaseKookClient client, RequestOptions? options) => + MessageHelper.DeleteAsync(messageId, client, options); - public static Task DeleteDirectMessageAsync(IMessageChannel channel, Guid messageId, BaseKookClient client, - RequestOptions options) - => MessageHelper.DeleteDirectAsync(messageId, client, options); + public static Task DeleteDirectMessageAsync(IMessageChannel channel, + Guid messageId, BaseKookClient client, RequestOptions? options) => + MessageHelper.DeleteDirectAsync(messageId, client, options); - public static async Task ModifyMessageAsync(IMessageChannel channel, Guid messageId, Action func, - BaseKookClient client, RequestOptions options) - => await MessageHelper.ModifyAsync(messageId, client, func, options).ConfigureAwait(false); + public static async Task ModifyMessageAsync(IMessageChannel channel, Guid messageId, + Action func, BaseKookClient client, RequestOptions? options) => + await MessageHelper.ModifyAsync(messageId, client, func, options).ConfigureAwait(false); #endregion #region Direct Messages - public static async Task GetDirectMessageAsync(IDMChannel channel, BaseKookClient client, - Guid id, RequestOptions options) + public static async Task GetDirectMessageAsync(IDMChannel channel, + BaseKookClient client, Guid id, RequestOptions? options) { - DirectMessage model = await client.ApiClient.GetDirectMessageAsync(id, channel.Id, options: options) - .ConfigureAwait(false); - if (model == null) return null; - - // User userModel = await client.ApiClient.GetUserAsync(model.AuthorId, options); - // var author = MessageHelper.GetAuthor(client, null, userModel); + DirectMessage model = await client.ApiClient.GetDirectMessageAsync( + id, channel.Id, options: options).ConfigureAwait(false); return RestMessage.Create(client, channel, channel.Recipient, model); } public static IAsyncEnumerable> GetDirectMessagesAsync(IDMChannel channel, - BaseKookClient client, - Guid? referenceMessageId, Direction dir, int limit, bool includeReferenceMessage, RequestOptions options) + BaseKookClient client, Guid? referenceMessageId, Direction direction, int limit, bool includeReferenceMessage, + RequestOptions? options) { - if (dir == Direction.Around) // && limit > KookConfig.MaxMessagesPerBatch // Around mode returns error messages from endpoint + if (direction == Direction.Around) { int around = limit / 2; if (referenceMessageId.HasValue) { - IAsyncEnumerable> messages = GetDirectMessagesAsync(channel, client, referenceMessageId, - Direction.Before, around, - includeReferenceMessage, options); - messages = messages.Concat(GetDirectMessagesAsync(channel, client, referenceMessageId, Direction.After, + IAsyncEnumerable> before = GetDirectMessagesAsync( + channel, client, referenceMessageId, Direction.Before, + around, includeReferenceMessage, options); + return before.Concat(GetDirectMessagesAsync( + channel, client, referenceMessageId, Direction.After, around, false, options)); - return messages; } - else // Shouldn't happen since there's no public overload for Guid? and Direction - return GetDirectMessagesAsync(channel, client, null, Direction.Before, around + 1, - includeReferenceMessage, options); + + return GetDirectMessagesAsync(channel, client, null, Direction.Before, + around + 1, includeReferenceMessage, options); } return new PagedAsyncEnumerable( KookConfig.MaxMessagesPerBatch, - async (info, ct) => + async (info, _) => { - IReadOnlyCollection models = await client.ApiClient.QueryDirectMessagesAsync(channel.ChatCode, - referenceMessageId: info.Position, dir: dir, count: limit, options: options).ConfigureAwait(false); + IReadOnlyCollection models = await client.ApiClient + .QueryDirectMessagesAsync(channel.ChatCode, null, info.Position, direction, limit, options) + .ConfigureAwait(false); ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); // Insert the reference message before query results - if (includeReferenceMessage && info.Position.HasValue && dir == Direction.After) + if (includeReferenceMessage && info.Position.HasValue && direction == Direction.After) { - DirectMessage currentMessage = await client.ApiClient.GetDirectMessageAsync(info.Position.Value, - channel.ChatCode, options: options); - builder.Add(RestMessage.Create(client, channel, channel.Recipient, currentMessage)); + DirectMessage currentMessage = await client.ApiClient.GetDirectMessageAsync( + info.Position.Value, channel.ChatCode, options); + IUser author = MessageHelper.GetAuthor(client, channel, currentMessage); + builder.Add(RestMessage.Create(client, channel, author, currentMessage)); } - foreach (DirectMessage model in models) builder.Add(RestMessage.Create(client, channel, channel.Recipient, model)); + foreach (DirectMessage model in models) + { + IUser author = MessageHelper.GetAuthor(client, channel, model); + builder.Add(RestMessage.Create(client, channel, author, model)); + } // Append the reference message after query results - if (includeReferenceMessage && info.Position.HasValue && dir == Direction.Before) + if (includeReferenceMessage && info.Position.HasValue && direction == Direction.Before) { - DirectMessage currentMessage = await client.ApiClient.GetDirectMessageAsync(info.Position.Value, - channel.ChatCode, options: options); - builder.Add(RestMessage.Create(client, channel, channel.Recipient, currentMessage)); + DirectMessage currentMessage = await client.ApiClient.GetDirectMessageAsync( + info.Position.Value, channel.ChatCode, options); + IUser author = MessageHelper.GetAuthor(client, channel, currentMessage); + builder.Add(RestMessage.Create(client, channel, author, currentMessage)); } return builder.ToImmutable(); @@ -366,19 +363,9 @@ public static IAsyncEnumerable> GetDirectMessag { if (lastPage.Count != KookConfig.MaxMessagesPerBatch) return false; - if (dir == Direction.Before) -#if NET6_0_OR_GREATER - info.Position = lastPage.MinBy(x => x.Timestamp)?.Id; -#else - info.Position = lastPage.OrderBy(x => x.Timestamp).FirstOrDefault()?.Id; -#endif - else - -#if NET6_0_OR_GREATER - info.Position = lastPage.MaxBy(x => x.Timestamp)?.Id; -#else - info.Position = lastPage.OrderByDescending(x => x.Timestamp).FirstOrDefault()?.Id; -#endif + info.Position = direction == Direction.Before + ? lastPage.MinBy(x => x.Timestamp)?.Id + : lastPage.MaxBy(x => x.Timestamp)?.Id; return true; }, referenceMessageId, @@ -387,56 +374,72 @@ public static IAsyncEnumerable> GetDirectMessag } public static async Task> SendDirectMessageAsync(IDMChannel channel, - BaseKookClient client, MessageType messageType, string content, RequestOptions options, IQuote quote = null) + BaseKookClient client, MessageType messageType, string content, IQuote? quote, RequestOptions? options) { - CreateDirectMessageParams args = new(messageType, channel.Recipient.Id, content) { QuotedMessageId = quote?.QuotedMessageId }; - CreateDirectMessageResponse model = await client.ApiClient.CreateDirectMessageAsync(args, options).ConfigureAwait(false); + CreateDirectMessageParams args = new() + { + Type = messageType, + UserId = channel.Recipient.Id, + Content = content, + QuotedMessageId = quote?.QuotedMessageId + }; + CreateDirectMessageResponse model = await client.ApiClient + .CreateDirectMessageAsync(args, options).ConfigureAwait(false); return new Cacheable(null, model.MessageId, false, async () => await channel.GetMessageAsync(model.MessageId) as IUserMessage); } - public static async Task> SendDirectCardsAsync(IDMChannel channel, - BaseKookClient client, IEnumerable cards, RequestOptions options, IQuote quote = null) + public static Task> SendDirectCardsAsync(IDMChannel channel, + BaseKookClient client, IEnumerable cards, IQuote? quote, RequestOptions? options) { string json = MessageHelper.SerializeCards(cards); - return await SendDirectMessageAsync(channel, client, MessageType.Card, json, options, quote); + return SendDirectMessageAsync(channel, client, MessageType.Card, json, quote, options); } public static Task> SendDirectCardAsync(IDMChannel channel, - BaseKookClient client, ICard card, RequestOptions options, IQuote quote = null) - => SendDirectCardsAsync(channel, client, new[] { card }, options, quote); + BaseKookClient client, ICard card, IQuote? quote, RequestOptions? options) => + SendDirectCardsAsync(channel, client, [card], quote, options); public static async Task> SendDirectFileAsync(IDMChannel channel, - BaseKookClient client, string path, string fileName, AttachmentType type, RequestOptions options, - IQuote quote = null) + BaseKookClient client, string path, string? filename, AttachmentType type, IQuote? quote, + RequestOptions? options) { - using FileAttachment file = new(path, fileName, type); - return await SendDirectFileAsync(channel, client, file, options, quote); + using FileAttachment file = new(path, filename, type); + return await SendDirectFileAsync(channel, client, file, quote, options); } public static async Task> SendDirectFileAsync(IDMChannel channel, - BaseKookClient client, Stream stream, string fileName, AttachmentType type, RequestOptions options, - IQuote quote = null) + BaseKookClient client, Stream stream, string filename, AttachmentType type, IQuote? quote, + RequestOptions? options) { - using FileAttachment file = new(stream, fileName, type); - return await SendDirectFileAsync(channel, client, file, options, quote); + using FileAttachment file = new(stream, filename, type); + return await SendDirectFileAsync(channel, client, file, quote, options); } public static async Task> SendDirectFileAsync(IDMChannel channel, - BaseKookClient client, FileAttachment attachment, RequestOptions options, IQuote quote = null) + BaseKookClient client, FileAttachment attachment, IQuote? quote, RequestOptions? options) { switch (attachment.Mode) { case CreateAttachmentMode.FilePath: case CreateAttachmentMode.Stream: + { + if (attachment.Uri is not null) break; + if (attachment.Stream is null) + throw new ArgumentNullException(nameof(attachment.Stream), "The stream cannot be null."); + CreateAssetParams createAssetParams = new() + { + File = attachment.Stream, + FileName = attachment.FileName + }; CreateAssetResponse assetResponse = await client.ApiClient - .CreateAssetAsync(new CreateAssetParams { File = attachment.Stream, FileName = attachment.FileName }, options); + .CreateAssetAsync(createAssetParams, options).ConfigureAwait(false); attachment.Uri = new Uri(assetResponse.Url); + } break; case CreateAttachmentMode.AssetUri: - if (!UrlValidation.ValidateKookAssetUrl(attachment.Uri.OriginalString)) + if (attachment.Uri is null || !UrlValidation.ValidateKookAssetUrl(attachment.Uri.OriginalString)) throw new ArgumentException("The uri cannot be blank.", nameof(attachment.Uri)); - break; default: throw new ArgumentOutOfRangeException(nameof(attachment.Mode), attachment.Mode, "Unknown attachment mode"); @@ -444,105 +447,125 @@ public static async Task> SendDirectFileAsync(IDMC return attachment.Type switch { - AttachmentType.File => await SendDirectMessageAsync(channel, client, MessageType.File, - attachment.Uri.OriginalString, options, quote), - AttachmentType.Image => await SendDirectMessageAsync(channel, client, MessageType.Image, - attachment.Uri.OriginalString, options, quote), - AttachmentType.Video => await SendDirectMessageAsync(channel, client, MessageType.Video, - attachment.Uri.OriginalString, options, quote), - AttachmentType.Audio => await SendDirectCardAsync(channel, client, - new CardBuilder().WithSize(CardSize.Large) - .WithTheme(CardTheme.None) - .AddModule(x => x - .WithSource(attachment.Uri.OriginalString) - .WithTitle(attachment.FileName)) - .Build(), options, quote), + AttachmentType.File => await SendAttachmentAsync(MessageType.File).ConfigureAwait(false), + AttachmentType.Image => await SendAttachmentAsync(MessageType.Image).ConfigureAwait(false), + AttachmentType.Video => await SendAttachmentAsync(MessageType.Video).ConfigureAwait(false), + AttachmentType.Audio => await SendDirectCardAsync( + channel, client, + new CardBuilder(theme: CardTheme.None, size: CardSize.Large) + .AddModule(new AudioModuleBuilder(attachment.Uri.OriginalString, attachment.FileName)) + .Build(), + quote, options).ConfigureAwait(false), _ => throw new ArgumentOutOfRangeException(nameof(attachment.Type), attachment.Type, "Unknown attachment type") }; + + Task> SendAttachmentAsync(MessageType messageType) => + SendDirectMessageAsync(channel, client, messageType, attachment.Uri.OriginalString, quote, options); } - public static async Task ModifyDirectMessageAsync(IDMChannel channel, Guid messageId, - Action func, - BaseKookClient client, RequestOptions options) - => await MessageHelper.ModifyDirectAsync(messageId, client, func, options).ConfigureAwait(false); + public static Task ModifyDirectMessageAsync(IDMChannel channel, Guid messageId, + Action func, BaseKookClient client, RequestOptions? options) => + MessageHelper.ModifyDirectAsync(messageId, client, func, options); #endregion #region Permission Overwrites public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, - BaseKookClient client, - IUser user, RequestOptions options) + BaseKookClient client, IUser user, RequestOptions? options) { - CreateOrRemoveChannelPermissionOverwriteParams args = new(channel.Id, - PermissionOverwriteTargetType.User, - user.Id); - CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient.CreateChannelPermissionOverwriteAsync(args, options) + CreateOrRemoveChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.User, + TargetId = user.Id + }; + CreateOrModifyChannelPermissionOverwriteResponse response = await client.ApiClient + .CreateChannelPermissionOverwriteAsync(args, options) .ConfigureAwait(false); - return new UserPermissionOverwrite(user, new OverwritePermissions(resp.Allow, resp.Deny)); + return new UserPermissionOverwrite(user, new OverwritePermissions(response.Allow, response.Deny)); } public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, - BaseKookClient client, - IRole role, RequestOptions options) + BaseKookClient client, IRole role, RequestOptions? options) { - CreateOrRemoveChannelPermissionOverwriteParams args = new(channel.Id, PermissionOverwriteTargetType.Role, role.Id); - CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient.CreateChannelPermissionOverwriteAsync(args, options) + CreateOrRemoveChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.Role, + TargetId = role.Id + }; + CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient + .CreateChannelPermissionOverwriteAsync(args, options) .ConfigureAwait(false); return new RolePermissionOverwrite(role.Id, new OverwritePermissions(resp.Allow, resp.Deny)); } - public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseKookClient client, - IUser user, RequestOptions options) + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, + BaseKookClient client, IUser user, RequestOptions? options) { - CreateOrRemoveChannelPermissionOverwriteParams args = new(channel.Id, - PermissionOverwriteTargetType.User, - user.Id); + CreateOrRemoveChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.User, + TargetId = user.Id + }; await client.ApiClient.RemoveChannelPermissionOverwriteAsync(args, options).ConfigureAwait(false); } - public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseKookClient client, - IRole role, RequestOptions options) + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, + BaseKookClient client, IRole role, RequestOptions? options) { - CreateOrRemoveChannelPermissionOverwriteParams args = new(channel.Id, - PermissionOverwriteTargetType.Role, - role.Id); + CreateOrRemoveChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.Role, + TargetId = role.Id + }; await client.ApiClient.RemoveChannelPermissionOverwriteAsync(args, options).ConfigureAwait(false); } public static async Task ModifyPermissionOverwriteAsync(IGuildChannel channel, - BaseKookClient client, - IGuildUser user, Func func, RequestOptions options) + BaseKookClient client, IGuildUser user, Func func, + RequestOptions? options) { OverwritePermissions? perms = channel.UserPermissionOverwrites.SingleOrDefault(x => x.Target.Id == user.Id)?.Permissions; if (!perms.HasValue) - throw new ArgumentNullException(nameof(user), - "The user does not have any permission overwrites on this channel."); - + throw new ArgumentNullException(nameof(user), "The user does not have any permission overwrites on this channel."); perms = func(perms.Value); - ModifyChannelPermissionOverwriteParams args = new(channel.Id, PermissionOverwriteTargetType.User, - user.Id, - perms.Value.AllowValue, perms.Value.DenyValue); - CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient.ModifyChannelPermissionOverwriteAsync(args, options) + ModifyChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.User, + TargetId = user.Id, + Allow = perms.Value.AllowValue, + Deny = perms.Value.DenyValue + }; + CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient + .ModifyChannelPermissionOverwriteAsync(args, options) .ConfigureAwait(false); return new UserPermissionOverwrite(user, new OverwritePermissions(resp.Allow, resp.Deny)); } public static async Task ModifyPermissionOverwriteAsync(IGuildChannel channel, - BaseKookClient client, - IRole role, Func func, RequestOptions options) + BaseKookClient client, IRole role, Func func, + RequestOptions? options) { OverwritePermissions? perms = channel.RolePermissionOverwrites.SingleOrDefault(x => x.Target == role.Id)?.Permissions; if (!perms.HasValue) - throw new ArgumentNullException(nameof(role), - "The role does not have any permission overwrites on this channel."); - + throw new ArgumentNullException(nameof(role), "The role does not have any permission overwrites on this channel."); perms = func(perms.Value); - ModifyChannelPermissionOverwriteParams args = new(channel.Id, PermissionOverwriteTargetType.Role, - role.Id, - perms.Value.AllowValue, perms.Value.DenyValue); - CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient.ModifyChannelPermissionOverwriteAsync(args, options) + ModifyChannelPermissionOverwriteParams args = new() + { + ChannelId = channel.Id, + TargetType = PermissionOverwriteTargetType.Role, + TargetId = role.Id, + Allow = perms.Value.AllowValue, + Deny = perms.Value.DenyValue + }; + CreateOrModifyChannelPermissionOverwriteResponse resp = await client.ApiClient + .ModifyChannelPermissionOverwriteAsync(args, options) .ConfigureAwait(false); return new RolePermissionOverwrite(role.Id, new OverwritePermissions(resp.Allow, resp.Deny)); } @@ -552,62 +575,63 @@ public static async Task ModifyPermissionOverwriteAsync #region Invites public static async Task> GetInvitesAsync(IGuildChannel channel, - BaseKookClient client, - RequestOptions options) + BaseKookClient client, RequestOptions? options) { - IEnumerable models = await client.ApiClient.GetGuildInvitesAsync(channel.GuildId, channel.Id, options: options) - .FlattenAsync().ConfigureAwait(false); + IEnumerable models = await client.ApiClient + .GetGuildInvitesAsync(channel.GuildId, channel.Id, options: options) + .FlattenAsync() + .ConfigureAwait(false); return models.Select(x => RestInvite.Create(client, channel.Guild, channel, x)).ToImmutableArray(); } - /// - /// may not be equal to zero. - /// and must be greater than zero. - /// must be lesser than 604800. - /// public static async Task CreateInviteAsync(IGuildChannel channel, BaseKookClient client, - int? maxAge, int? maxUses, RequestOptions options) + int? maxAge, int? maxUses, RequestOptions? options) { CreateGuildInviteParams args = new() { - GuildId = channel.GuildId, ChannelId = channel.Id, MaxAge = (InviteMaxAge)(maxAge ?? 0), MaxUses = (InviteMaxUses)(maxUses ?? -1) + GuildId = channel.GuildId, + ChannelId = channel.Id, + MaxAge = (InviteMaxAge)(maxAge ?? 0), + MaxUses = (InviteMaxUses)(maxUses ?? -1) }; - CreateGuildInviteResponse model = await client.ApiClient.CreateGuildInviteAsync(args, options).ConfigureAwait(false); - IEnumerable invites = await client.ApiClient.GetGuildInvitesAsync(channel.GuildId, channel.Id, options: options) + CreateGuildInviteResponse model = await client.ApiClient + .CreateGuildInviteAsync(args, options).ConfigureAwait(false); + IEnumerable invites = await client.ApiClient + .GetGuildInvitesAsync(channel.GuildId, channel.Id, options: options) .FlattenAsync().ConfigureAwait(false); - return RestInvite.Create(client, channel.Guild, channel, invites.SingleOrDefault(x => x.Url == model.Url)); + return RestInvite.Create(client, channel.Guild, channel, invites.Single(x => x.Url == model.Url)); } public static Task CreateInviteAsync(IGuildChannel channel, BaseKookClient client, - InviteMaxAge maxAge, InviteMaxUses maxUses, RequestOptions options) => + InviteMaxAge maxAge, InviteMaxUses maxUses, RequestOptions? options) => CreateInviteAsync(channel, client, (int?)maxAge, (int?)maxUses, options); #endregion #region Categories - public static async Task GetCategoryAsync(INestedChannel channel, BaseKookClient client, - RequestOptions options) + public static async Task GetCategoryAsync(INestedChannel channel, BaseKookClient client, + RequestOptions? options) { // if no category id specified, return null - if (!channel.CategoryId.HasValue) return null; + if (!channel.CategoryId.HasValue) + return null; // CategoryId will contain a value here - Channel model = await client.ApiClient.GetGuildChannelAsync(channel.CategoryId.Value, options) + Model model = await client.ApiClient + .GetGuildChannelAsync(channel.CategoryId.Value, options) .ConfigureAwait(false); return RestChannel.Create(client, model) as ICategoryChannel; } - /// This channel does not have a parent channel. - public static async Task SyncPermissionsAsync(INestedChannel channel, BaseKookClient client, - RequestOptions options) + public static Task SyncPermissionsAsync(INestedChannel channel, BaseKookClient client, + RequestOptions? options) { - var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false); - if (category == null) - throw new InvalidOperationException("This channel does not have a parent channel."); - - var args = new SyncChannelPermissionsParams(channel.Id); - await client.ApiClient.SyncChannelPermissionsAsync(args, options).ConfigureAwait(false); + SyncChannelPermissionsParams args = new() + { + ChannelId = channel.Id + }; + return client.ApiClient.SyncChannelPermissionsAsync(args, options); } #endregion @@ -615,32 +639,33 @@ public static async Task SyncPermissionsAsync(INestedChannel channel, BaseKookCl #region Users /// Resolving permissions requires the parent guild to be downloaded. - public static async Task GetUserAsync(IGuildChannel channel, IGuild guild, BaseKookClient client, - ulong id, RequestOptions options) + public static async Task GetUserAsync(IGuildChannel channel, + IGuild guild, BaseKookClient client, ulong id, RequestOptions? options) { - GuildMember model = await client.ApiClient.GetGuildMemberAsync(channel.GuildId, id, options).ConfigureAwait(false); - if (model == null) return null; + GuildMember model = await client.ApiClient + .GetGuildMemberAsync(channel.GuildId, id, options) + .ConfigureAwait(false); RestGuildUser user = RestGuildUser.Create(client, guild, model); - if (!user.GetPermissions(channel).ViewChannel) return null; - - return user; + return user.GetPermissions(channel).ViewChannel + ? user + : null; } /// Resolving permissions requires the parent guild to be downloaded. public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, - IGuild guild, BaseKookClient client, - int limit, int fromPage, RequestOptions options) => + IGuild guild, BaseKookClient client, int limit, int fromPage, RequestOptions? options) => client.ApiClient.GetGuildMembersAsync(guild.Id, limit: limit, fromPage: fromPage, options: options) - .Select(x => x.Select(y => RestGuildUser.Create(client, guild, y)) + .Select(x => x + .Select(y => RestGuildUser.Create(client, guild, y)) .Where(y => y.GetPermissions(channel).ViewChannel) .ToImmutableArray() as IReadOnlyCollection); public static async Task> GetConnectedUsersAsync(IVoiceChannel channel, - IGuild guild, BaseKookClient client, RequestOptions options) + IGuild guild, BaseKookClient client, RequestOptions? options) { IReadOnlyCollection model = await client.ApiClient.GetConnectedUsersAsync(channel.Id, options); - return model?.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); + return model.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); } #endregion diff --git a/src/Kook.Net.Rest/Entities/Channels/IRestAudioChannel.cs b/src/Kook.Net.Rest/Entities/Channels/IRestAudioChannel.cs index 823d5541..00043ae0 100644 --- a/src/Kook.Net.Rest/Entities/Channels/IRestAudioChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/IRestAudioChannel.cs @@ -3,6 +3,4 @@ namespace Kook.Rest; /// /// Represents a generic REST-based audio channel. /// -public interface IRestAudioChannel : IAudioChannel -{ -} +public interface IRestAudioChannel : IAudioChannel; diff --git a/src/Kook.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Kook.Net.Rest/Entities/Channels/IRestMessageChannel.cs index d70dcbc9..564c7021 100644 --- a/src/Kook.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -18,7 +18,7 @@ public interface IRestMessageChannel : IMessageChannel /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - Task GetMessageAsync(Guid id, RequestOptions options = null); + Task GetMessageAsync(Guid id, RequestOptions? options = null); /// /// Gets the last N messages from this message channel. @@ -32,7 +32,8 @@ public interface IRestMessageChannel : IMessageChannel /// /// Paged collection of messages. /// - IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null); + IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null); /// /// Gets a collection of messages in this channel. @@ -49,7 +50,7 @@ public interface IRestMessageChannel : IMessageChannel /// Paged collection of messages. /// IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null); /// /// Gets a collection of messages in this channel. @@ -66,5 +67,5 @@ IAsyncEnumerable> GetMessagesAsync(Guid referen /// Paged collection of messages. /// IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null); } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestCategoryChannel.cs index 725abe5b..be2a2853 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -6,7 +6,7 @@ namespace Kook.Rest; /// /// Represents a REST-based category channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestCategoryChannel : RestGuildChannel, ICategoryChannel { #region RestCategoryChannel @@ -32,13 +32,13 @@ internal RestCategoryChannel(BaseKookClient kook, IGuild guild, ulong id) /// /// This method is not supported with category channels. - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + throw new NotSupportedException(); /// /// This method is not supported with category channels. - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + throw new NotSupportedException(); #endregion } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestChannel.cs index a0be31b0..9f741d37 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestChannel.cs @@ -17,8 +17,7 @@ internal RestChannel(BaseKookClient kook, ulong id) internal static RestChannel Create(BaseKookClient kook, Model model) => model.Type switch { - ChannelType.Text or ChannelType.Voice - => RestGuildChannel.Create(kook, new RestGuild(kook, model.GuildId), model), + ChannelType.Text or ChannelType.Voice => RestGuildChannel.Create(kook, new RestGuild(kook, model.GuildId), model), ChannelType.Category => RestCategoryChannel.Create(kook, new RestGuild(kook, model.GuildId), model), _ => new RestChannel(kook, model.Id) }; @@ -26,8 +25,7 @@ ChannelType.Text or ChannelType.Voice internal static RestChannel Create(BaseKookClient kook, Model model, IGuild guild) => model.Type switch { - ChannelType.Text or ChannelType.Voice - => RestGuildChannel.Create(kook, guild, model), + ChannelType.Text or ChannelType.Voice => RestGuildChannel.Create(kook, guild, model), ChannelType.Category => RestCategoryChannel.Create(kook, guild, model), _ => new RestChannel(kook, model.Id) }; @@ -37,22 +35,22 @@ internal virtual void Update(Model model) } /// - public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); + public virtual Task UpdateAsync(RequestOptions? options = null) => Task.Delay(0); #endregion #region IChannel /// - string IChannel.Name => null; + string IChannel.Name => string.Empty; /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overridden + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); //Overridden /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + AsyncEnumerable.Empty>(); //Overridden #endregion } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestDMChannel.cs index 11167407..3b08d2a2 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -7,7 +7,7 @@ namespace Kook.Rest; /// /// Represents a REST-based direct-message channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel { #region RestDMChannel @@ -39,14 +39,19 @@ public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRest /// /// Gets a collection that is the current logged-in user and the recipient. /// - public IReadOnlyCollection Users => ImmutableArray.Create(CurrentUser, Recipient); + public IReadOnlyCollection Users => [CurrentUser, Recipient]; internal RestDMChannel(BaseKookClient kook, Guid chatCode, ulong recipientId) - : base(kook, default(ulong)) + : base(kook, default) { Id = chatCode; Recipient = new RestUser(Kook, recipientId); - CurrentUser = new RestUser(Kook, kook.CurrentUser.Id); + if (kook.CurrentUser is RestUser restUser) + CurrentUser = restUser; + else if (kook.CurrentUser is not null) + CurrentUser = new RestUser(Kook, kook.CurrentUser.Id); + else + throw new InvalidOperationException("The current user is not set well via login."); } internal static RestDMChannel Create(BaseKookClient kook, Model model) @@ -59,15 +64,15 @@ internal static RestDMChannel Create(BaseKookClient kook, Model model) internal void Update(Model model) => Recipient.Update(model.Recipient); /// - public override async Task UpdateAsync(RequestOptions options = null) + public override async Task UpdateAsync(RequestOptions? options = null) { Model model = await Kook.ApiClient.GetUserChatAsync(Id, options).ConfigureAwait(false); Update(model); } /// - public Task CloseAsync(RequestOptions options = null) - => ChannelHelper.DeleteDMChannelAsync(this, Kook, options); + public Task CloseAsync(RequestOptions? options = null) => + ChannelHelper.DeleteDMChannelAsync(this, Kook, options); /// /// Gets a user in this channel from the provided . @@ -76,14 +81,10 @@ public Task CloseAsync(RequestOptions options = null) /// /// A object that is a recipient of this channel; otherwise null. /// - public RestUser GetUser(ulong id) + public RestUser? GetUser(ulong id) { - if (id == Recipient.Id) - return Recipient; - else if (id == Kook.CurrentUser.Id) - return CurrentUser; - else - return null; + if (id == Recipient.Id) return Recipient; + return id == Kook.CurrentUser?.Id ? CurrentUser : null; } /// @@ -96,8 +97,9 @@ public RestUser GetUser(ulong id) /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendTextAsync(string text, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectMessageAsync(this, Kook, MessageType.KMarkdown, text, options, quote); + public Task> SendTextAsync(string text, IQuote? quote = null, + RequestOptions? options = null) => + ChannelHelper.SendDirectMessageAsync(this, Kook, MessageType.KMarkdown, text, quote, options); /// /// Sends a file to this message channel. @@ -106,7 +108,7 @@ public Task> SendTextAsync(string text, IQuote quo /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The file path of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. @@ -114,9 +116,9 @@ public Task> SendTextAsync(string text, IQuote quo /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendFileAsync(string path, string fileName = null, - AttachmentType type = AttachmentType.File, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, path, fileName, type, options, quote); + public Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, path, filename, type, quote, options); /// /// Sends a file to this message channel. @@ -125,7 +127,7 @@ public Task> SendFileAsync(string path, string fil /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The stream of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. @@ -133,9 +135,9 @@ public Task> SendFileAsync(string path, string fil /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendFileAsync(Stream stream, string fileName = null, - AttachmentType type = AttachmentType.File, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, stream, fileName, type, options, quote); + public Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, stream, filename, type, quote, options); /// /// Sends a file to this message channel. @@ -151,8 +153,8 @@ public Task> SendFileAsync(Stream stream, string f /// contains the identifier and timestamp of the sent message. /// public Task> SendFileAsync(FileAttachment attachment, - IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, attachment, options, quote); + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, attachment, quote, options); /// /// Sends a card message to this message channel. @@ -164,8 +166,9 @@ public Task> SendFileAsync(FileAttachment attachme /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectCardsAsync(this, Kook, cards, options, quote); + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectCardsAsync(this, Kook, cards, quote, options); /// /// Sends a card message to this message channel. @@ -177,43 +180,44 @@ public Task> SendCardsAsync(IEnumerable car /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendCardAsync(ICard card, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectCardAsync(this, Kook, card, options, quote); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectCardAsync(this, Kook, card, quote, options); /// - public Task GetMessageAsync(Guid id, RequestOptions options = null) - => ChannelHelper.GetDirectMessageAsync(this, Kook, id, options); + public Task GetMessageAsync(Guid id, RequestOptions? options = null) => + ChannelHelper.GetDirectMessageAsync(this, Kook, id, options); /// - public IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); + public IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); /// public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); /// public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); #endregion #region Messages /// - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) - => ChannelHelper.DeleteDirectMessageAsync(this, messageId, Kook, options); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + ChannelHelper.DeleteDirectMessageAsync(this, messageId, Kook, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteDirectMessageAsync(this, message.Id, Kook, options); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + ChannelHelper.DeleteDirectMessageAsync(this, message.Id, Kook, options); /// - public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) - => ChannelHelper.ModifyDirectMessageAsync(this, messageId, func, Kook, options); + public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions? options = null) => + ChannelHelper.ModifyDirectMessageAsync(this, messageId, func, Kook, options); #endregion @@ -227,86 +231,75 @@ public Task ModifyMessageAsync(Guid messageId, Action func, R #region IRestPrivateChannel /// - IReadOnlyCollection IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + IReadOnlyCollection IRestPrivateChannel.Recipients => [Recipient]; #endregion #region IPrivateChannel /// - IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + IReadOnlyCollection IPrivateChannel.Recipients => [Recipient]; #endregion #region IMessageChannel /// - async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetMessageAsync(id, options).ConfigureAwait(false) + : null; /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, + RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(limit, options) + : AsyncEnumerable.Empty>(); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit, - CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(referenceMessageId, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(referenceMessageId, dir, limit, options) + : AsyncEnumerable.Empty>(); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit, - CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(referenceMessage, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(referenceMessage, dir, limit, options) + : AsyncEnumerable.Empty>(); /// - Task> IMessageChannel.SendFileAsync(string path, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(path, fileName, type, quote, options); + Task> IMessageChannel.SendFileAsync(string path, string? filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(path, filename, type, quote, options); /// - Task> IMessageChannel.SendFileAsync(Stream stream, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(stream, fileName, type, quote, options); + Task> IMessageChannel.SendFileAsync(Stream stream, string filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(stream, filename, type, quote, options); /// Task> IMessageChannel.SendFileAsync(FileAttachment attachment, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(attachment, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(attachment, quote, options); /// Task> IMessageChannel.SendTextAsync(string text, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendTextAsync(text, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendTextAsync(text, quote, options); /// Task> IMessageChannel.SendCardsAsync(IEnumerable cards, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardsAsync(cards, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardsAsync(cards, quote, options); /// Task> IMessageChannel.SendCardAsync(ICard card, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardAsync(card, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardAsync(card, quote, options); #endregion @@ -323,12 +316,12 @@ Task> IMessageChannel.SendCardAsync(ICard card, #region IChannel /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + ImmutableArray.Create>(Users).ToAsyncEnumerable(); #endregion } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestGuildChannel.cs index d2f7b3bb..ec71d0a3 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -11,8 +11,8 @@ public class RestGuildChannel : RestChannel, IGuildChannel { #region RestGuildChannel - private ImmutableArray _rolePermissionOverwrites; - private ImmutableArray _userPermissionOverwrites; + private ImmutableArray _rolePermissionOverwrites = []; + private ImmutableArray _userPermissionOverwrites = []; /// public virtual IReadOnlyCollection RolePermissionOverwrites => _rolePermissionOverwrites; @@ -48,6 +48,7 @@ internal RestGuildChannel(BaseKookClient kook, IGuild guild, ulong id) { Guild = guild; Type = ChannelType.Unspecified; + Name = string.Empty; } internal static RestGuildChannel Create(BaseKookClient kook, IGuild guild, Model model) => @@ -73,31 +74,23 @@ internal override void Update(Model model) Position = model.Position; CreatorId = model.CreatorId; - if (model.UserPermissionOverwrites is not null) - { - API.UserPermissionOverwrite[] overwrites = model.UserPermissionOverwrites; - ImmutableArray.Builder newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); - for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(overwrites[i].ToEntity()); - - _userPermissionOverwrites = newOverwrites.ToImmutable(); - } + IEnumerable userPermissionOverwrites = model + .UserPermissionOverwrites + .Select(x => x.ToEntity(Kook)); + _userPermissionOverwrites = [..userPermissionOverwrites]; - if (model.RolePermissionOverwrites is not null) - { - API.RolePermissionOverwrite[] overwrites = model.RolePermissionOverwrites; - ImmutableArray.Builder newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); - for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(overwrites[i].ToEntity()); - - _rolePermissionOverwrites = newOverwrites.ToImmutable(); - } + IEnumerable rolePermissionOverwrites = model + .RolePermissionOverwrites + .Select(x => x.ToEntity()); + _rolePermissionOverwrites = [..rolePermissionOverwrites]; } internal void Update(MentionedChannel model) => Name = model.Name; /// - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions? options = null) { - Channel model = await ChannelHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); + Model model = await ChannelHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); Update(model); } @@ -113,17 +106,17 @@ public async Task ModifyAsync(Action func, Request /// /// A task that represents the asynchronous get operation. The task result contains the creator of this channel. /// - public Task GetCreatorAsync(RequestOptions options = null) - => ClientHelper.GetUserAsync(Kook, CreatorId, options); + public Task GetCreatorAsync(RequestOptions? options = null) => + ClientHelper.GetUserAsync(Kook, CreatorId, options); /// - public Task DeleteAsync(RequestOptions options = null) - => ChannelHelper.DeleteGuildChannelAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + ChannelHelper.DeleteGuildChannelAsync(this, Kook, options); /// - public override async Task UpdateAsync(RequestOptions options = null) + public override async Task UpdateAsync(RequestOptions? options = null) { - Channel model = await Kook.ApiClient.GetGuildChannelAsync(Id, options).ConfigureAwait(false); + Model model = await Kook.ApiClient.GetGuildChannelAsync(Id, options).ConfigureAwait(false); Update(model); } @@ -134,14 +127,8 @@ public override async Task UpdateAsync(RequestOptions options = null) /// /// An overwrite object for the targeted user; null if none is set. /// - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - return _userPermissionOverwrites[i].Permissions; - - return null; - } + public OverwritePermissions? GetPermissionOverwrite(IUser user) => + _userPermissionOverwrites.FirstOrDefault(x => x.Target.Id == user.Id)?.Permissions; /// /// Gets the permission overwrite for a specific role. @@ -150,14 +137,8 @@ public override async Task UpdateAsync(RequestOptions options = null) /// /// An overwrite object for the targeted role; null if none is set. /// - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - return _rolePermissionOverwrites[i].Permissions; - - return null; - } + public OverwritePermissions? GetPermissionOverwrite(IRole role) => + _rolePermissionOverwrites.FirstOrDefault(x => x.Target == role.Id)?.Permissions; /// /// Adds the permission overwrite for the given user. @@ -167,10 +148,12 @@ public override async Task UpdateAsync(RequestOptions options = null) /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) + public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) { - UserPermissionOverwrite perms = await ChannelHelper.AddPermissionOverwriteAsync(this, Kook, user, options).ConfigureAwait(false); - _userPermissionOverwrites = _userPermissionOverwrites.Add(perms); + UserPermissionOverwrite permissionOverwrite = await ChannelHelper + .AddPermissionOverwriteAsync(this, Kook, user, options) + .ConfigureAwait(false); + _userPermissionOverwrites = [.._userPermissionOverwrites, permissionOverwrite]; } /// @@ -181,10 +164,12 @@ public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions op /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null) + public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null) { - RolePermissionOverwrite perms = await ChannelHelper.AddPermissionOverwriteAsync(this, Kook, role, options).ConfigureAwait(false); - _rolePermissionOverwrites = _rolePermissionOverwrites.Add(perms); + RolePermissionOverwrite permissionOverwrite = await ChannelHelper + .AddPermissionOverwriteAsync(this, Kook, role, options) + .ConfigureAwait(false); + _rolePermissionOverwrites = [.._rolePermissionOverwrites, permissionOverwrite]; } /// @@ -195,16 +180,10 @@ public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions options /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) + public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Kook, user, options).ConfigureAwait(false); - - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - { - _userPermissionOverwrites = _userPermissionOverwrites.RemoveAt(i); - return; - } + _userPermissionOverwrites = [.._userPermissionOverwrites.Where(x => x.Target.Id != user.Id)]; } /// @@ -215,16 +194,10 @@ public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Kook, role, options).ConfigureAwait(false); - - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - { - _rolePermissionOverwrites = _rolePermissionOverwrites.RemoveAt(i); - return; - } + _rolePermissionOverwrites = [.._rolePermissionOverwrites.Where(x => x.Target != role.Id)]; } /// @@ -236,18 +209,13 @@ public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions opti /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options = null) + public async Task ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options = null) { - UserPermissionOverwrite perms = await ChannelHelper.ModifyPermissionOverwriteAsync(this, Kook, user, func, options).ConfigureAwait(false); - - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - { - _userPermissionOverwrites = _userPermissionOverwrites.RemoveAt(i); - _userPermissionOverwrites = _userPermissionOverwrites.Add(perms); - return; - } + UserPermissionOverwrite permission = await ChannelHelper + .ModifyPermissionOverwriteAsync(this, Kook, user, func, options) + .ConfigureAwait(false); + _userPermissionOverwrites = [.._userPermissionOverwrites.Where(x => x.Target.Id != user.Id), permission]; } /// @@ -259,17 +227,13 @@ public async Task ModifyPermissionOverwriteAsync(IGuildUser user, Func /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null) + public async Task ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options = null) { - RolePermissionOverwrite perms = await ChannelHelper.ModifyPermissionOverwriteAsync(this, Kook, role, func, options).ConfigureAwait(false); - - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - { - _rolePermissionOverwrites = _rolePermissionOverwrites.RemoveAt(i); - _rolePermissionOverwrites = _rolePermissionOverwrites.Add(perms); - return; - } + RolePermissionOverwrite permission = await ChannelHelper + .ModifyPermissionOverwriteAsync(this, Kook, role, func, options) + .ConfigureAwait(false); + _rolePermissionOverwrites = [.._rolePermissionOverwrites.Where(x => x.Target != role.Id), permission]; } /// @@ -285,77 +249,66 @@ public async Task ModifyPermissionOverwriteAsync(IRole role, Func - IGuild IGuildChannel.Guild - { - get - { - if (Guild != null) return Guild; - - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } + IGuild IGuildChannel.Guild => Guild; /// - OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) - => GetPermissionOverwrite(role); + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); /// - OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) - => GetPermissionOverwrite(user); + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); /// - async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, RequestOptions options) - => await AddPermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, RequestOptions? options) => + await AddPermissionOverwriteAsync(role, options).ConfigureAwait(false); /// - async Task IGuildChannel.AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options) - => await AddPermissionOverwriteAsync(user, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options) => + await AddPermissionOverwriteAsync(user, options).ConfigureAwait(false); /// - async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) - => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions? options) => + await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); /// - async Task IGuildChannel.RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options) - => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options) => + await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); /// - async Task IGuildChannel.ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options) - => await ModifyPermissionOverwriteAsync(role, func, options).ConfigureAwait(false); + async Task IGuildChannel.ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options) => + await ModifyPermissionOverwriteAsync(role, func, options).ConfigureAwait(false); /// - async Task IGuildChannel.ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options) - => await ModifyPermissionOverwriteAsync(user, func, options).ConfigureAwait(false); + async Task IGuildChannel.ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options) => + await ModifyPermissionOverwriteAsync(user, func, options).ConfigureAwait(false); /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden in Text/Voice + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => + AsyncEnumerable.Empty>(); //Overridden in Text/Voice /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overridden in Text/Voice + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); //Overridden in Text/Voice /// - async Task IGuildChannel.GetCreatorAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetCreatorAsync(options).ConfigureAwait(false); - else - return null; - } + async Task IGuildChannel.GetCreatorAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetCreatorAsync(options).ConfigureAwait(false) + : null; #endregion #region IChannel /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden in Text/Voice + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + AsyncEnumerable.Empty>(); //Overridden in Text/Voice /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overridden in Text/Voice + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); //Overridden in Text/Voice #endregion } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestTextChannel.cs index aea68127..4ef8fed2 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -6,7 +6,7 @@ namespace Kook.Rest; /// /// Represents a REST-based channel in a guild that can send and receive messages. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel { #region RestTextChannel @@ -33,6 +33,7 @@ internal RestTextChannel(BaseKookClient kook, IGuild guild, ulong id) : base(kook, guild, id) { Type = ChannelType.Text; + Topic = string.Empty; } internal static new RestTextChannel Create(BaseKookClient kook, IGuild guild, Model model) @@ -47,18 +48,22 @@ internal override void Update(Model model) { base.Update(model); CategoryId = model.CategoryId != 0 ? model.CategoryId : null; - Topic = model.Topic; + Topic = model.Topic ?? string.Empty; SlowModeInterval = model.SlowMode / 1000; IsPermissionSynced = model.PermissionSync; } /// - public virtual async Task ModifyAsync(Action func, RequestOptions options = null) + public virtual async Task ModifyAsync(Action func, RequestOptions? options = null) { Model model = await ChannelHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); Update(model); } + /// + public override Task UpdateAsync(RequestOptions? options = null) => + ChannelHelper.UpdateAsync(this, Kook, options); + /// /// Gets a user in this channel. /// @@ -71,8 +76,8 @@ public virtual async Task ModifyAsync(Action func, /// A task representing the asynchronous get operation. The task result contains a guild user object that /// represents the user; null if none is found. /// - public Task GetUserAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetUserAsync(this, Guild, Kook, id, options); + public Task GetUserAsync(ulong id, RequestOptions? options = null) => + ChannelHelper.GetUserAsync(this, Guild, Kook, id, options); /// /// Gets a collection of users that are able to view the channel. @@ -86,61 +91,68 @@ public Task GetUserAsync(ulong id, RequestOptions options = null) /// paginated response into a collection of users with /// is required if you wish to access the users. /// - public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) - => ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options); + public IAsyncEnumerable> GetUsersAsync(RequestOptions? options = null) => + ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options); /// - public Task GetMessageAsync(Guid id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Kook, id, options); + public Task GetMessageAsync(Guid id, RequestOptions? options = null) => + ChannelHelper.GetMessageAsync(this, Kook, id, options); /// - public virtual IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); + public virtual IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); /// - public virtual IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); + public virtual IAsyncEnumerable> GetMessagesAsync( + Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + RequestOptions? options = null) => + ChannelHelper.GetMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); /// - public virtual IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); + public virtual IAsyncEnumerable> GetMessagesAsync( + IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + RequestOptions? options = null) => + ChannelHelper.GetMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); /// - public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Kook, options); + public virtual Task> GetPinnedMessagesAsync(RequestOptions? options = null) => + ChannelHelper.GetPinnedMessagesAsync(this, Kook, options); /// - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - Quote quote = null, IUser ephemeralUser = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, path, fileName, type, options, quote, ephemeralUser); + public Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) + { + string name = filename ?? Path.GetFileName(path); + return ChannelHelper.SendFileAsync(this, Kook, path, name, type, quote, ephemeralUser, options); + } /// - public Task> SendFileAsync(Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, - Quote quote = null, IUser ephemeralUser = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, stream, fileName, type, options, quote, ephemeralUser); + public Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) => + ChannelHelper.SendFileAsync(this, Kook, stream, filename, type, quote, ephemeralUser, options); /// - public Task> SendFileAsync(FileAttachment attachment, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, attachment, options, quote, ephemeralUser); + public Task> SendFileAsync(FileAttachment attachment, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendFileAsync(this, Kook, attachment, quote, ephemeralUser, options); /// - public Task> SendTextAsync(string text, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Kook, MessageType.KMarkdown, text, options, quote, ephemeralUser); + public Task> SendTextAsync(string text, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendMessageAsync(this, Kook, MessageType.KMarkdown, text, quote, ephemeralUser, options); /// - public Task> SendCardsAsync(IEnumerable cards, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendCardsAsync(this, Kook, cards, options, quote, ephemeralUser); + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendCardsAsync(this, Kook, cards, quote, ephemeralUser, options); /// - public Task> SendCardAsync(ICard card, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendCardAsync(this, Kook, card, options, quote, ephemeralUser); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendCardAsync(this, Kook, card, quote, ephemeralUser, options); /// /// Gets the parent (category) channel of this channel. @@ -150,29 +162,30 @@ public Task> SendCardAsync(ICard card, Quote quote /// A task that represents the asynchronous get operation. The task result contains the category channel /// representing the parent of this channel; null if none is set. /// - public Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Kook, options); + public Task GetCategoryAsync(RequestOptions? options = null) => + ChannelHelper.GetCategoryAsync(this, Kook, options); /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Kook, options); + public Task SyncPermissionsAsync(RequestOptions? options = null) => + ChannelHelper.SyncPermissionsAsync(this, Kook, options); #endregion #region Invites /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); + public async Task> GetInvitesAsync(RequestOptions? options = null) => + await ChannelHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, + RequestOptions? options = null) => + await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => + await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); #endregion @@ -181,38 +194,30 @@ public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge. #region IChannel /// - async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetUserAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetUserAsync(id, options).ConfigureAwait(false) + : null; /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); #endregion #region IGuildChannel /// - async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetUserAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetUserAsync(id, options).ConfigureAwait(false) + : null; /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => mode == CacheMode.AllowDownload ? GetUsersAsync(options) : AsyncEnumerable.Empty>(); @@ -222,101 +227,88 @@ IAsyncEnumerable> IGuildChannel.GetUsersAsync(Ca #region IMessageChannel /// - Task> IMessageChannel.SendFileAsync(string path, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(path, fileName, type, (Quote)quote, ephemeralUser, options); + Task> IMessageChannel.SendFileAsync(string path, string? filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(path, filename, type, (Quote?)quote, ephemeralUser, options); /// - Task> IMessageChannel.SendFileAsync(Stream stream, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(stream, fileName, type, (Quote)quote, ephemeralUser, options); + Task> IMessageChannel.SendFileAsync(Stream stream, string filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(stream, filename, type, quote, ephemeralUser, options); /// Task> IMessageChannel.SendFileAsync(FileAttachment attachment, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(attachment, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(attachment, quote, ephemeralUser, options); /// Task> IMessageChannel.SendTextAsync(string text, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendTextAsync(text, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendTextAsync(text, quote, ephemeralUser, options); /// Task> IMessageChannel.SendCardAsync(ICard card, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardAsync(card, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardAsync(card, quote, ephemeralUser, options); /// Task> IMessageChannel.SendCardsAsync(IEnumerable cards, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardsAsync(cards, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardsAsync(cards, quote, ephemeralUser, options); /// - async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetMessageAsync(id, options).ConfigureAwait(false) + : null; /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, + CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(limit, options) + : AsyncEnumerable.Empty>(); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit, - CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(referenceMessageId, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(referenceMessageId, dir, limit, options) + : AsyncEnumerable.Empty>(); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit, - CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(referenceMessage, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetMessagesAsync(referenceMessage, dir, limit, options) + : AsyncEnumerable.Empty>(); /// - async Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + async Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions? options) => + await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, messageId, Kook, options); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + ChannelHelper.DeleteMessageAsync(this, messageId, Kook, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, message.Id, Kook, options); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + ChannelHelper.DeleteMessageAsync(this, message.Id, Kook, options); /// - public async Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) - => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); + public async Task ModifyMessageAsync(Guid messageId, + Action func, RequestOptions? options = null) => + await ChannelHelper.ModifyMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); #endregion #region INestedChannel /// - async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) - { - if (CategoryId.HasValue && mode == CacheMode.AllowDownload) - return await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false) as ICategoryChannel; - - return null; - } + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions? options) => + CategoryId.HasValue && mode == CacheMode.AllowDownload + ? await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false) as ICategoryChannel + : null; #endregion } diff --git a/src/Kook.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Kook.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 8be7e213..67cacfde 100644 --- a/src/Kook.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Kook.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -8,7 +8,7 @@ namespace Kook.Rest; /// /// Represents a REST-based voice channel in a guild. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel { #region RestVoiceChannel @@ -17,16 +17,16 @@ public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChanne public VoiceQuality? VoiceQuality { get; private set; } /// - public int? UserLimit { get; private set; } + public int UserLimit { get; private set; } /// - public string ServerUrl { get; private set; } + public string? ServerUrl { get; private set; } /// public bool? IsVoiceRegionOverwritten { get; private set; } /// - public string VoiceRegion { get; private set; } + public string? VoiceRegion { get; private set; } /// public bool HasPassword { get; private set; } @@ -57,19 +57,25 @@ internal override void Update(Model model) } /// - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions? options = null) { Model model = await ChannelHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); Update(model); } + /// + public override Task UpdateAsync(RequestOptions? options = null) => + ChannelHelper.UpdateAsync(this, Kook, options); + /// /// Gets the users connected to this voice channel. /// /// The options to be used when sending the request. - /// A task that represents the asynchronous get operation. The task result contains a collection of users. - public async Task> GetConnectedUsersAsync(RequestOptions options) - => await ChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false); + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of users. + /// + public async Task> GetConnectedUsersAsync(RequestOptions? options) => + await ChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false); #endregion @@ -79,26 +85,28 @@ public async Task> GetConnectedUsersAsync(RequestOpti /// /// Getting messages from a voice channel is not supported. - public override IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + public override IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. - public override IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + public override IAsyncEnumerable> GetMessagesAsync( + Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. - public override IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + public override IAsyncEnumerable> GetMessagesAsync( + IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. - public override Task> GetPinnedMessagesAsync(RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + public override Task> GetPinnedMessagesAsync(RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); #endregion @@ -106,7 +114,8 @@ public override Task> GetPinnedMessagesAsync(Re /// /// Connecting to a REST-based channel is not supported. - Task IAudioChannel.ConnectAsync(/*bool selfDeaf, bool selfMute, */bool external, bool disconnect) => + Task IAudioChannel.ConnectAsync( /*bool selfDeaf, bool selfMute, */ + bool external, bool disconnect) => throw new NotSupportedException(); /// @@ -118,11 +127,12 @@ Task IAudioChannel.ConnectAsync(/*bool selfDeaf, bool selfMute, */ #region IGuildChannel /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => AsyncEnumerable.Empty>(); #endregion @@ -130,25 +140,20 @@ IAsyncEnumerable> IGuildChannel.GetUsersAsync(Ca #region INestedChannel /// - async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) - { - if (CategoryId.HasValue && mode == CacheMode.AllowDownload) - return await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false) as ICategoryChannel; - - return null; - } + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions? options) => + CategoryId.HasValue && mode == CacheMode.AllowDownload + ? await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false) as ICategoryChannel + : null; #endregion #region IVoiceChannel - async Task> IVoiceChannel.GetConnectedUsersAsync(CacheMode mode, RequestOptions options) - { - if (mode is CacheMode.AllowDownload) - return await ChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false); - else - return ImmutableArray.Create(); - } + async Task> IVoiceChannel.GetConnectedUsersAsync( + CacheMode mode, RequestOptions? options) => + mode is CacheMode.AllowDownload + ? await ChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false) + : ImmutableArray.Create(); #endregion } diff --git a/src/Kook.Net.Rest/Entities/Games/GameHelper.cs b/src/Kook.Net.Rest/Entities/Games/GameHelper.cs index 2a1c4aa6..6d5be106 100644 --- a/src/Kook.Net.Rest/Entities/Games/GameHelper.cs +++ b/src/Kook.Net.Rest/Entities/Games/GameHelper.cs @@ -5,16 +5,24 @@ namespace Kook.Rest; internal static class GameHelper { - public static async Task DeleteAsync(IGame game, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteAsync(IGame game, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteGameAsync(game.Id, options).ConfigureAwait(false); - public static async Task ModifyAsync(IGame game, BaseKookClient client, Action func, - RequestOptions options) + public static async Task ModifyAsync(IGame game, BaseKookClient client, + Action func, RequestOptions? options) { - GameProperties properties = new() { Name = game.Name, IconUrl = game.Icon }; + GameProperties properties = new() + { + Name = game.Name, + IconUrl = game.Icon + }; func(properties); - ModifyGameParams args = new() { Id = game.Id, Name = properties.Name, Icon = properties.IconUrl }; + ModifyGameParams args = new() + { + Id = game.Id, + Name = properties.Name, + Icon = properties.IconUrl + }; Game model = await client.ApiClient.ModifyGameAsync(args, options).ConfigureAwait(false); return RestGame.Create(client, model); } diff --git a/src/Kook.Net.Rest/Entities/Games/RestGame.cs b/src/Kook.Net.Rest/Entities/Games/RestGame.cs index 210edcdc..981c6237 100644 --- a/src/Kook.Net.Rest/Entities/Games/RestGame.cs +++ b/src/Kook.Net.Rest/Entities/Games/RestGame.cs @@ -10,8 +10,8 @@ namespace Kook.Rest; [DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestGame : RestEntity, IGame { - private ImmutableArray _productNames; - private ImmutableArray _processNames; + private ImmutableArray _productNames = []; + private ImmutableArray _processNames = []; /// public string Name { get; private set; } @@ -20,13 +20,13 @@ public class RestGame : RestEntity, IGame public GameType GameType { get; private set; } /// - public string Options { get; private set; } + public string? Options { get; private set; } /// public bool RequireAdminPrivilege { get; private set; } /// - public string Icon { get; private set; } + public string? Icon { get; private set; } /// public IReadOnlyCollection ProductNames => _productNames.ToReadOnlyCollection(); @@ -37,6 +37,7 @@ public class RestGame : RestEntity, IGame internal RestGame(BaseKookClient kook, int id) : base(kook, id) { + Name = string.Empty; } internal static RestGame Create(BaseKookClient kook, Model model) @@ -53,20 +54,20 @@ internal void Update(Model model) Options = model.Options; RequireAdminPrivilege = model.KmHookAdmin; Icon = model.Icon; - _productNames = model.ProductNames.ToImmutableArray(); - _processNames = model.ProcessNames.ToImmutableArray(); + _productNames = [..model.ProductNames]; + _processNames = [..model.ProcessNames]; } /// - public async Task ModifyAsync(Action func, RequestOptions options = null) => - await GameHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + GameHelper.ModifyAsync(this, Kook, func, options); /// - public async Task DeleteAsync(RequestOptions options = null) => await GameHelper.DeleteAsync(this, Kook, options).ConfigureAwait(false); + public Task DeleteAsync(RequestOptions? options = null) => GameHelper.DeleteAsync(this, Kook, options); /// - async Task IGame.ModifyAsync(Action func, RequestOptions options) - => await ModifyAsync(func, options); + async Task IGame.ModifyAsync(Action func, RequestOptions? options) => + await ModifyAsync(func, options); private string DebuggerDisplay => $"{Name} ({Id}, {GameType.ToString()})"; } diff --git a/src/Kook.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Kook.Net.Rest/Entities/Guilds/GuildHelper.cs index 3cd0a060..71bf6739 100644 --- a/src/Kook.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Kook.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -8,15 +8,15 @@ internal static class GuildHelper { #region General - public static async Task LeaveAsync(IGuild guild, BaseKookClient client, - RequestOptions options) => + public static async Task LeaveAsync(IGuild guild, BaseKookClient client, RequestOptions? options) => await client.ApiClient.LeaveGuildAsync(guild.Id, options).ConfigureAwait(false); public static async Task>> GetBoostSubscriptionsAsync( - IGuild guild, BaseKookClient client, RequestOptions options) + IGuild guild, BaseKookClient client, RequestOptions? options) { IEnumerable subscriptions = await client.ApiClient - .GetGuildBoostSubscriptionsAsync(guild.Id, options: options).FlattenAsync(); + .GetGuildBoostSubscriptionsAsync(guild.Id, options: options) + .FlattenAsync().ConfigureAwait(false); return subscriptions.GroupBy(x => x.UserId) .ToImmutableDictionary(x => RestUser.Create(client, x.First().User) as IUser, x => x.GroupBy(y => (y.StartTime, y.EndTime)) @@ -25,10 +25,11 @@ public static async Task>> GetActiveBoostSubscriptionsAsync( - IGuild guild, BaseKookClient client, RequestOptions options) + IGuild guild, BaseKookClient client, RequestOptions? options) { IEnumerable subscriptions = await client.ApiClient - .GetGuildBoostSubscriptionsAsync(guild.Id, DateTimeOffset.Now.Add(-KookConfig.BoostPackDuration), options: options).FlattenAsync(); + .GetGuildBoostSubscriptionsAsync(guild.Id, DateTimeOffset.Now.Add(-KookConfig.BoostPackDuration), options: options) + .FlattenAsync().ConfigureAwait(false); return subscriptions.GroupBy(x => x.UserId) .ToImmutableDictionary(x => RestUser.Create(client, x.First().User) as IUser, x => x.GroupBy(y => (y.StartTime, y.EndTime)) @@ -72,13 +73,21 @@ public static ulong GetUploadLimit(IGuild guild) #region Invites public static async Task> GetInvitesAsync(IGuild guild, BaseKookClient client, - RequestOptions options) + RequestOptions? options) { - IEnumerable models = - await client.ApiClient.GetGuildInvitesAsync(guild.Id, null, options: options).FlattenAsync().ConfigureAwait(false); - return models.Select(x => RestInvite.Create(client, guild, x.ChannelId.HasValue - ? guild.GetChannelAsync(x.ChannelId.Value, CacheMode.CacheOnly).GetAwaiter().GetResult() - : null, x)).ToImmutableArray(); + IEnumerable models = await client.ApiClient + .GetGuildInvitesAsync(guild.Id, options: options) + .FlattenAsync().ConfigureAwait(false); + ImmutableArray.Builder invites = ImmutableArray.CreateBuilder(); + foreach (Invite model in models) + { + IGuildChannel? cachedChannel = await guild + .GetChannelAsync(model.ChannelId, CacheMode.CacheOnly) + .ConfigureAwait(false); + IGuildChannel guildChannel = cachedChannel ?? new RestGuildChannel(client, guild, model.ChannelId); + invites.Add(RestInvite.Create(client, guild, guildChannel, model)); + } + return invites.ToImmutable(); } /// @@ -87,35 +96,44 @@ public static async Task> GetInvitesAsync(IGuild /// must be lesser than 604800. /// public static async Task CreateInviteAsync(IGuild guild, BaseKookClient client, - int? maxAge, int? maxUses, RequestOptions options) + int? maxAge, int? maxUses, RequestOptions? options) { - CreateGuildInviteParams args = new() { GuildId = guild.Id, MaxAge = (InviteMaxAge)(maxAge ?? 0), MaxUses = (InviteMaxUses)(maxUses ?? -1) }; - CreateGuildInviteResponse model = await client.ApiClient.CreateGuildInviteAsync(args, options).ConfigureAwait(false); - IEnumerable invites = await client.ApiClient.GetGuildInvitesAsync(guild.Id, options: options).FlattenAsync().ConfigureAwait(false); - Invite invite = invites.SingleOrDefault(x => x.Url == model.Url); - return RestInvite.Create(client, guild, invite?.ChannelId.HasValue ?? false - ? guild.GetChannelAsync(invite.ChannelId.Value, CacheMode.CacheOnly).GetAwaiter().GetResult() - : null, invite); + CreateGuildInviteParams args = new() + { + GuildId = guild.Id, + MaxAge = (InviteMaxAge)(maxAge ?? 0), + MaxUses = (InviteMaxUses)(maxUses ?? -1) + }; + CreateGuildInviteResponse model = await client.ApiClient + .CreateGuildInviteAsync(args, options).ConfigureAwait(false); + Invite invite = await client.ApiClient + .GetGuildInvitesAsync(guild.Id, options: options) + .Flatten() + .SingleAsync(x => x.Url == model.Url).ConfigureAwait(false); + IGuildChannel? cachedChannel = await guild + .GetChannelAsync(invite.ChannelId, CacheMode.CacheOnly) + .ConfigureAwait(false); + IGuildChannel guildChannel = cachedChannel ?? new RestGuildChannel(client, guild, invite.ChannelId); + return RestInvite.Create(client, guild, guildChannel, invite); } public static Task CreateInviteAsync(IGuild channel, BaseKookClient client, - InviteMaxAge maxAge, InviteMaxUses maxUses, RequestOptions options) => + InviteMaxAge maxAge, InviteMaxUses maxUses, RequestOptions? options) => CreateInviteAsync(channel, client, (int?)maxAge, (int?)maxUses, options); #endregion #region Roles - /// is null. public static async Task CreateRoleAsync(IGuild guild, BaseKookClient client, - string name, RequestOptions options) + string? name, RequestOptions? options) { - if (name == null) throw new ArgumentNullException(nameof(name)); - - CreateGuildRoleParams createGuildRoleParams = new() { Name = name, GuildId = guild.Id }; - + CreateGuildRoleParams createGuildRoleParams = new() + { + Name = name, + GuildId = guild.Id + }; Role model = await client.ApiClient.CreateGuildRoleAsync(createGuildRoleParams, options).ConfigureAwait(false); - return RestRole.Create(client, guild, model); } @@ -123,103 +141,121 @@ public static async Task CreateRoleAsync(IGuild guild, BaseKookClient #region Users - public static IAsyncEnumerable> GetUsersAsync(IGuild guild, BaseKookClient client, int limit, int fromPage, - RequestOptions options) => - client.ApiClient.GetGuildMembersAsync(guild.Id, limit: limit, fromPage: fromPage, options: options) - .Select(x => x - .Select(y => RestGuildUser.Create(client, guild, y)).ToImmutableArray() as IReadOnlyCollection); + public static IAsyncEnumerable> GetUsersAsync(IGuild guild, + BaseKookClient client, int limit, int fromPage, RequestOptions? options) => + client.ApiClient + .GetGuildMembersAsync(guild.Id, limit: limit, fromPage: fromPage, options: options) + .Select(x => + x.Select(y => RestGuildUser.Create(client, guild, y)).ToImmutableArray() + as IReadOnlyCollection); public static async Task GetUserAsync(IGuild guild, BaseKookClient client, - ulong id, RequestOptions options) + ulong id, RequestOptions? options) { GuildMember model = await client.ApiClient.GetGuildMemberAsync(guild.Id, id, options).ConfigureAwait(false); - if (model != null) return RestGuildUser.Create(client, guild, model); - - return null; + return RestGuildUser.Create(client, guild, model); } - public static async Task<(IReadOnlyCollection Muted, IReadOnlyCollection Deafened)> GetGuildMutedDeafenedUsersAsync(IGuild guild, - BaseKookClient client, - RequestOptions options) + public static async Task<(IReadOnlyCollection Muted, IReadOnlyCollection Deafened)> GetGuildMutedDeafenedUsersAsync( + IGuild guild, BaseKookClient client, RequestOptions? options) { - GetGuildMuteDeafListResponse models = await client.ApiClient.GetGuildMutedDeafenedUsersAsync(guild.Id, options).ConfigureAwait(false); + GetGuildMuteDeafListResponse models = await client.ApiClient + .GetGuildMutedDeafenedUsersAsync(guild.Id, options).ConfigureAwait(false); return (models.Muted.UserIds, models.Deafened.UserIds); } - public static async Task MuteUserAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task MuteUserAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { - CreateOrRemoveGuildMuteDeafParams args = new() { GuildId = user.GuildId, UserId = user.Id, Type = MuteOrDeafType.Mute }; + CreateOrRemoveGuildMuteDeafParams args = new() + { + GuildId = user.GuildId, + UserId = user.Id, + Type = MuteOrDeafType.Mute + }; await client.ApiClient.CreateGuildMuteDeafAsync(args, options).ConfigureAwait(false); } - public static async Task DeafenUserAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task DeafenUserAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { - CreateOrRemoveGuildMuteDeafParams args = new() { GuildId = user.GuildId, UserId = user.Id, Type = MuteOrDeafType.Deaf }; + CreateOrRemoveGuildMuteDeafParams args = new() + { + GuildId = user.GuildId, + UserId = user.Id, + Type = MuteOrDeafType.Deaf + }; await client.ApiClient.CreateGuildMuteDeafAsync(args, options).ConfigureAwait(false); } - public static async Task UnmuteUserAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task UnmuteUserAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { - CreateOrRemoveGuildMuteDeafParams args = new() { GuildId = user.GuildId, UserId = user.Id, Type = MuteOrDeafType.Mute }; + CreateOrRemoveGuildMuteDeafParams args = new() + { + GuildId = user.GuildId, + UserId = user.Id, + Type = MuteOrDeafType.Mute + }; await client.ApiClient.RemoveGuildMuteDeafAsync(args, options).ConfigureAwait(false); } - public static async Task UndeafenUserAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task UndeafenUserAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { - CreateOrRemoveGuildMuteDeafParams args = new() { GuildId = user.GuildId, UserId = user.Id, Type = MuteOrDeafType.Deaf }; + CreateOrRemoveGuildMuteDeafParams args = new() + { + GuildId = user.GuildId, + UserId = user.Id, + Type = MuteOrDeafType.Deaf + }; await client.ApiClient.RemoveGuildMuteDeafAsync(args, options).ConfigureAwait(false); } - public static IAsyncEnumerable> SearchUsersAsync(IGuild guild, BaseKookClient client, - Action func, int limit, RequestOptions options) - { - IAsyncEnumerable> models = client.ApiClient.GetGuildMembersAsync(guild.Id, func, limit, 1, options); - return models.Select(x => x.Select(y => RestGuildUser.Create(client, guild, y)).ToImmutableArray() as IReadOnlyCollection); - } + public static IAsyncEnumerable> SearchUsersAsync(IGuild guild, + BaseKookClient client, Action func, int limit, RequestOptions? options) => + client.ApiClient + .GetGuildMembersAsync(guild.Id, func, limit, 1, options) + .Select(x => x.Select(y => RestGuildUser.Create(client, guild, y)).ToImmutableArray() + as IReadOnlyCollection); #endregion #region Channels public static async Task GetChannelAsync(IGuild guild, BaseKookClient client, - ulong id, RequestOptions options) + ulong id, RequestOptions? options) { Channel model = await client.ApiClient.GetGuildChannelAsync(id, options).ConfigureAwait(false); - if (model != null) return RestGuildChannel.Create(client, guild, model); - - return null; + return RestGuildChannel.Create(client, guild, model); } - public static async Task> GetChannelsAsync(IGuild guild, BaseKookClient client, - RequestOptions options) + public static async Task> GetChannelsAsync(IGuild guild, + BaseKookClient client, RequestOptions? options) { - IEnumerable models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options: options).FlattenAsync().ConfigureAwait(false); + IEnumerable models = await client.ApiClient + .GetGuildChannelsAsync(guild.Id, options: options).FlattenAsync().ConfigureAwait(false); return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); } - /// is null. public static async Task CreateTextChannelAsync(IGuild guild, BaseKookClient client, - string name, RequestOptions options, Action func) + string name, Action? func, RequestOptions? options) { - if (name == null) throw new ArgumentNullException(nameof(name)); - CreateTextChannelProperties props = new(); func?.Invoke(props); - - CreateGuildChannelParams args = new() { Name = name, CategoryId = props.CategoryId, GuildId = guild.Id, Type = ChannelType.Text }; + CreateGuildChannelParams args = new() + { + Name = name, + CategoryId = props.CategoryId, + GuildId = guild.Id, + Type = ChannelType.Text + }; Channel model = await client.ApiClient.CreateGuildChannelAsync(args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); } /// is null. public static async Task CreateVoiceChannelAsync(IGuild guild, BaseKookClient client, - string name, RequestOptions options, Action func) + string name, Action? func, RequestOptions? options) { - if (name == null) throw new ArgumentNullException(nameof(name)); - CreateVoiceChannelProperties props = new(); func?.Invoke(props); - CreateGuildChannelParams args = new() { Name = name, @@ -233,17 +269,17 @@ public static async Task CreateVoiceChannelAsync(IGuild guild, return RestVoiceChannel.Create(client, guild, model); } - /// is null. public static async Task CreateCategoryChannelAsync(IGuild guild, BaseKookClient client, - string name, RequestOptions options, Action func = null) + string name, Action? func, RequestOptions? options) { - if (name == null) throw new ArgumentNullException(nameof(name)); - CreateCategoryChannelProperties props = new(); func?.Invoke(props); - - CreateGuildChannelParams args = new() { GuildId = guild.Id, Name = name, IsCategory = 1 }; - + CreateGuildChannelParams args = new() + { + GuildId = guild.Id, + Name = name, + IsCategory = 1 + }; Channel model = await client.ApiClient.CreateGuildChannelAsync(args, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, guild, model); } @@ -253,30 +289,43 @@ public static async Task CreateCategoryChannelAsync(IGuild #region Bans public static async Task> GetBansAsync(IGuild guild, BaseKookClient client, - RequestOptions options) + RequestOptions? options) { - IReadOnlyCollection models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); + IReadOnlyCollection models = await client.ApiClient + .GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); + return [..models.Select(x => RestBan.Create(client, x))]; } - public static async Task GetBanAsync(IGuild guild, BaseKookClient client, ulong userId, RequestOptions options) + public static async Task GetBanAsync(IGuild guild, BaseKookClient client, + ulong userId, RequestOptions? options) { - IReadOnlyCollection models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); - Ban model = models.FirstOrDefault(x => x.User.Id == userId); - return model == null ? null : RestBan.Create(client, model); + IReadOnlyCollection models = await client.ApiClient + .GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); + Ban? model = models.FirstOrDefault(x => x.User.Id == userId); + return model is not null ? RestBan.Create(client, model) : null; } public static async Task AddBanAsync(IGuild guild, BaseKookClient client, - ulong userId, int pruneDays, string reason, RequestOptions options) + ulong userId, int pruneDays, string? reason, RequestOptions? options) { - CreateGuildBanParams args = new() { DeleteMessageDays = pruneDays, Reason = reason, GuildId = guild.Id, UserId = userId }; + CreateGuildBanParams args = new() + { + DeleteMessageDays = pruneDays, + Reason = reason, + GuildId = guild.Id, + UserId = userId + }; await client.ApiClient.CreateGuildBanAsync(args, options).ConfigureAwait(false); } public static async Task RemoveBanAsync(IGuild guild, BaseKookClient client, - ulong userId, RequestOptions options) + ulong userId, RequestOptions? options) { - RemoveGuildBanParams args = new() { GuildId = guild.Id, UserId = userId }; + RemoveGuildBanParams args = new() + { + GuildId = guild.Id, + UserId = userId + }; await client.ApiClient.RemoveGuildBanAsync(args, options).ConfigureAwait(false); } @@ -284,43 +333,59 @@ public static async Task RemoveBanAsync(IGuild guild, BaseKookClient client, #region Emotes - public static async Task> GetEmotesAsync(IGuild guild, BaseKookClient client, RequestOptions options) + public static async Task> GetEmotesAsync(IGuild guild, + BaseKookClient client, RequestOptions? options) { - IEnumerable models = await client.ApiClient.GetGuildEmotesAsync(guild.Id, options: options).FlattenAsync().ConfigureAwait(false); - return models.Select(x => x.ToEntity(guild.Id)).ToImmutableArray(); + IEnumerable models = await client.ApiClient + .GetGuildEmotesAsync(guild.Id, options: options) + .FlattenAsync().ConfigureAwait(false); + return [..models.Select(x => x.ToEntity(guild.Id))]; } - public static async Task GetEmoteAsync(IGuild guild, BaseKookClient client, string id, RequestOptions options) + public static async Task GetEmoteAsync(IGuild guild, BaseKookClient client, + string id, RequestOptions? options) { - IEnumerable emote = await client.ApiClient.GetGuildEmotesAsync(guild.Id, options: options).FlattenAsync().ConfigureAwait(false); - return emote.FirstOrDefault(x => x.Id == id)?.ToEntity(guild.Id); + API.Emoji? emote = await client.ApiClient + .GetGuildEmotesAsync(guild.Id, options: options) + .Flatten() + .FirstOrDefaultAsync(x => x.Id == id) + .ConfigureAwait(false); + return emote?.ToEntity(guild.Id); } - public static async Task CreateEmoteAsync(IGuild guild, BaseKookClient client, string name, Image image, RequestOptions options) + public static async Task CreateEmoteAsync(IGuild guild, BaseKookClient client, string name, Image image, RequestOptions? options) { - CreateGuildEmoteParams args = new() { Name = name, File = image.Stream, GuildId = guild.Id }; - + CreateGuildEmoteParams args = new() + { + Name = name, + File = image.Stream, + GuildId = guild.Id + }; API.Emoji emote = await client.ApiClient.CreateGuildEmoteAsync(args, options).ConfigureAwait(false); return emote.ToEntity(guild.Id); } - /// is null. - public static async Task ModifyEmoteNameAsync(IGuild guild, BaseKookClient client, IEmote emote, string name, - RequestOptions options) + public static async Task ModifyEmoteNameAsync(IGuild guild, BaseKookClient client, + IEmote emote, string name, RequestOptions? options) { - if (name == null) throw new ArgumentNullException(nameof(name)); - ModifyGuildEmoteParams args = new() { Name = name, Id = emote.Id }; + ModifyGuildEmoteParams args = new() + { + Name = name, + Id = emote.Id + }; await client.ApiClient.ModifyGuildEmoteAsync(args, options).ConfigureAwait(false); } - public static async Task DeleteEmoteAsync(IGuild guild, BaseKookClient client, string id, RequestOptions options) - => await client.ApiClient.DeleteGuildEmoteAsync(id, options).ConfigureAwait(false); + public static async Task DeleteEmoteAsync(IGuild guild, BaseKookClient client, + string id, RequestOptions? options) => + await client.ApiClient.DeleteGuildEmoteAsync(id, options).ConfigureAwait(false); #endregion #region Badges - public static async Task GetBadgeAsync(IGuild guild, BaseKookClient client, BadgeStyle style, RequestOptions options) => + public static async Task GetBadgeAsync(IGuild guild, BaseKookClient client, + BadgeStyle style, RequestOptions? options) => await client.ApiClient.GetGuildBadgeAsync(guild.Id, style, options).ConfigureAwait(false); #endregion diff --git a/src/Kook.Net.Rest/Entities/Guilds/RecommendInfo.cs b/src/Kook.Net.Rest/Entities/Guilds/RecommendInfo.cs index 357ad909..f02431f9 100644 --- a/src/Kook.Net.Rest/Entities/Guilds/RecommendInfo.cs +++ b/src/Kook.Net.Rest/Entities/Guilds/RecommendInfo.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Model = Kook.API.RecommendInfo; namespace Kook.Rest; @@ -11,7 +12,7 @@ public class RecommendInfo : IRecommendInfo public ulong GuildId { get; private set; } /// - public uint OpenId { get; private set; } + public uint? OpenId { get; private set; } /// public ulong DefaultChannelId { get; private set; } @@ -57,6 +58,12 @@ public class RecommendInfo : IRecommendInfo internal RecommendInfo() { + Name = string.Empty; + Icon = string.Empty; + Banner = string.Empty; + Description = string.Empty; + Tag = string.Empty; + CustomId = string.Empty; } internal static RecommendInfo Create(Model model) @@ -66,6 +73,15 @@ internal static RecommendInfo Create(Model model) return entity; } + [MemberNotNull( + nameof(Name), + nameof(Icon), + nameof(Banner), + nameof(Description), + nameof(Tag), + nameof(Features), + nameof(CustomId) + )] internal void Update(Model model) { // Update properties from model diff --git a/src/Kook.Net.Rest/Entities/Guilds/RestBan.cs b/src/Kook.Net.Rest/Entities/Guilds/RestBan.cs index 45fd293d..aa264cdd 100644 --- a/src/Kook.Net.Rest/Entities/Guilds/RestBan.cs +++ b/src/Kook.Net.Rest/Entities/Guilds/RestBan.cs @@ -6,7 +6,7 @@ namespace Kook.Rest; /// /// Represents a REST-based ban object. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestBan : IBan { #region RestBan @@ -32,7 +32,8 @@ internal RestBan(RestUser user, string reason, DateTimeOffset createdAt) CreatedAt = createdAt; } - internal static RestBan Create(BaseKookClient client, Model model) => new(RestUser.Create(client, model.User), model.Reason, model.CreatedAt); + internal static RestBan Create(BaseKookClient client, Model model) => + new(RestUser.Create(client, model.User), model.Reason, model.CreatedAt); /// /// Gets the name of the banned user. diff --git a/src/Kook.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Kook.Net.Rest/Entities/Guilds/RestGuild.cs index 4d3bc750..f2889191 100644 --- a/src/Kook.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Kook.Net.Rest/Entities/Guilds/RestGuild.cs @@ -10,7 +10,7 @@ namespace Kook.Rest; /// /// Represents a REST-based guild/server. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestGuild : RestEntity, IGuild, IUpdateable { #region RestGuild @@ -39,7 +39,7 @@ public class RestGuild : RestEntity, IGuild, IUpdateable /// /// Gets the nickname of the current user in this guild. /// - public string CurrentUserNickname { get; private set; } + public string? CurrentUserNickname { get; private set; } /// /// Gets the display name of the current user in this guild. @@ -80,7 +80,7 @@ public class RestGuild : RestEntity, IGuild, IUpdateable /// /// Gets the built-in role containing all users in this guild. /// - public RestRole EveryoneRole => GetRole(0); + public RestRole EveryoneRole => GetRole(0) ?? new RestRole(Kook, this, 0); /// /// @@ -103,8 +103,8 @@ public class RestGuild : RestEntity, IGuild, IUpdateable /// /// A read-only collection of message channels found within this guild. /// - public IReadOnlyCollection TextChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection TextChannels => + Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all voice channels in this guild. @@ -112,8 +112,8 @@ public IReadOnlyCollection TextChannels /// /// A read-only collection of voice channels found within this guild. /// - public IReadOnlyCollection VoiceChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection VoiceChannels => + Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all stage channels in this guild. @@ -127,8 +127,8 @@ public IReadOnlyCollection VoiceChannels /// /// A read-only collection of category channels found within this guild. /// - public IReadOnlyCollection CategoryChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection CategoryChannels => + Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all channels in this guild. @@ -165,15 +165,24 @@ public IReadOnlyCollection CategoryChannels /// /// TODO: To be documented. /// - public string AutoDeleteTime { get; private set; } + public string? AutoDeleteTime { get; private set; } /// - public RecommendInfo RecommendInfo { get; private set; } + public RecommendInfo? RecommendInfo { get; private set; } internal RestGuild(BaseKookClient client, ulong id) : base(client, id) { - _emotes = ImmutableArray.Create(); + _roles = ImmutableDictionary.Empty; + _currentUserRoles = []; + _channels = ImmutableDictionary.Empty; + _emotes = []; + Name = string.Empty; + Topic = string.Empty; + Icon = string.Empty; + Banner = string.Empty; + Region = string.Empty; + CurrentUserDisplayName = client.CurrentUser?.Username ?? string.Empty; } internal static RestGuild Create(BaseKookClient kook, RichModel model) @@ -202,17 +211,15 @@ internal void Update(RichModel model) Update(model as ExtendedModel); Banner = model.Banner; + if (Kook.CurrentUser is null) + throw new InvalidOperationException("The current user is not set well via login."); CurrentUserNickname = model.CurrentUserNickname == Kook.CurrentUser.Username ? null : model.CurrentUserNickname; CurrentUserDisplayName = CurrentUserNickname ?? Kook.CurrentUser.Username; - _currentUserRoles = model.CurrentUserRoles?.Select(GetRole).ToImmutableArray() ?? ImmutableArray.Create(); + if (model.CurrentUserRoles is not null) + _currentUserRoles = [..model.CurrentUserRoles.Select(GetRole).OfType()]; - if (model.Emojis != null) - { - ImmutableArray.Builder emotes = ImmutableArray.CreateBuilder(); - foreach (API.Emoji emoji in model.Emojis) - emotes.Add(emoji.ToEntity(model.Id)); - _emotes = emotes.ToImmutable(); - } + if (model.Emojis is { Length: > 0} ) + _emotes = [..model.Emojis.Select(x => x.ToEntity(model.Id))]; } internal void Update(ExtendedModel model) @@ -245,23 +252,25 @@ internal void Update(Model model) else DefaultChannelId = null; WelcomeChannelId = model.WelcomeChannelId != 0 ? model.WelcomeChannelId : null; - Available = true; - ImmutableDictionary.Builder roles = ImmutableDictionary.CreateBuilder(); - if (model.Roles != null) - for (int i = 0; i < model.Roles.Length; i++) - roles[model.Roles[i].Id] = RestRole.Create(Kook, this, model.Roles[i]); - - _roles = roles.ToImmutable(); - - ImmutableDictionary.Builder channels = ImmutableDictionary.CreateBuilder(); - if (model.Channels != null) - for (int i = 0; i < model.Channels.Length; i++) - channels[model.Channels[i].Id] = RestGuildChannel.Create(Kook, this, model.Channels[i]); + if (model.Roles is { Length: 0 }) + { + ImmutableDictionary.Builder roles = + ImmutableDictionary.CreateBuilder(); + foreach (API.Role roleModel in model.Roles) + roles[roleModel.Id] = RestRole.Create(Kook, this, roleModel); + _roles = roles.ToImmutable(); + } - _channels = channels.ToImmutable(); - _emotes = ImmutableArray.Create(); + if (model.Channels is not null) + { + ImmutableDictionary.Builder channels = + ImmutableDictionary.CreateBuilder(); + foreach (API.Channel channelModel in model.Channels) + channels[channelModel.Id] = RestGuildChannel.Create(Kook, this, channelModel); + _channels = channels.ToImmutable(); + } } #endregion @@ -269,24 +278,25 @@ internal void Update(Model model) #region Generals /// - public async Task UpdateAsync(RequestOptions options = null) + public async Task UpdateAsync(RequestOptions? options = null) { ExtendedModel model = await Kook.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false); Update(model); } /// - public Task LeaveAsync(RequestOptions options = null) - => GuildHelper.LeaveAsync(this, Kook, options); + public Task LeaveAsync(RequestOptions? options = null) => + GuildHelper.LeaveAsync(this, Kook, options); /// - public Task>> GetBoostSubscriptionsAsync(RequestOptions options = null) - => GuildHelper.GetBoostSubscriptionsAsync(this, Kook, options); + public Task>> GetBoostSubscriptionsAsync( + RequestOptions? options = null) => + GuildHelper.GetBoostSubscriptionsAsync(this, Kook, options); /// public Task>> GetActiveBoostSubscriptionsAsync( - RequestOptions options = null) - => GuildHelper.GetActiveBoostSubscriptionsAsync(this, Kook, options); + RequestOptions? options = null) => + GuildHelper.GetActiveBoostSubscriptionsAsync(this, Kook, options); #endregion @@ -301,8 +311,8 @@ public Task - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Kook, options); + public Task> GetBansAsync(RequestOptions? options = null) => + GuildHelper.GetBansAsync(this, Kook, options); /// /// Gets a ban object for a banned user. @@ -313,8 +323,8 @@ public Task> GetBansAsync(RequestOptions options = /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; null if the ban entry cannot be found. /// - public Task GetBanAsync(IUser user, RequestOptions options = null) - => GuildHelper.GetBanAsync(this, Kook, user.Id, options); + public Task GetBanAsync(IUser user, RequestOptions? options = null) => + GuildHelper.GetBanAsync(this, Kook, user.Id, options); /// /// Gets a ban object for a banned user. @@ -325,41 +335,40 @@ public Task GetBanAsync(IUser user, RequestOptions options = null) /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; null if the ban entry cannot be found. /// - public Task GetBanAsync(ulong userId, RequestOptions options = null) - => GuildHelper.GetBanAsync(this, Kook, userId, options); + public Task GetBanAsync(ulong userId, RequestOptions? options = null) => + GuildHelper.GetBanAsync(this, Kook, userId, options); /// - public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Kook, user.Id, pruneDays, reason, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string? reason = null, RequestOptions? options = null) => + GuildHelper.AddBanAsync(this, Kook, user.Id, pruneDays, reason, options); /// - public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Kook, userId, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string? reason = null, RequestOptions? options = null) => + GuildHelper.AddBanAsync(this, Kook, userId, pruneDays, reason, options); /// - public Task RemoveBanAsync(IUser user, RequestOptions options = null) - => GuildHelper.RemoveBanAsync(this, Kook, user.Id, options); + public Task RemoveBanAsync(IUser user, RequestOptions? options = null) => + GuildHelper.RemoveBanAsync(this, Kook, user.Id, options); /// - public Task RemoveBanAsync(ulong userId, RequestOptions options = null) - => GuildHelper.RemoveBanAsync(this, Kook, userId, options); + public Task RemoveBanAsync(ulong userId, RequestOptions? options = null) => + GuildHelper.RemoveBanAsync(this, Kook, userId, options); #endregion - // #region Invites - // - // /// - // /// Gets a collection of all invites in this guild. - // /// - // /// The options to be used when sending the request. - // /// - // /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - // /// invite metadata, each representing information for an invite found within this guild. - // /// - // public Task> GetInvitesAsync(RequestOptions options = null) - // => GuildHelper.GetInvitesAsync(this, Kook, options); - // - // #endregion + #region Invites + + /// + public async Task CreateInviteAsync(int? maxAge = 604800, + int? maxUses = null, RequestOptions? options = null) => + await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + + /// + public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => + await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + + #endregion #region Roles @@ -370,12 +379,7 @@ public Task RemoveBanAsync(ulong userId, RequestOptions options = null) /// /// A role that is associated with the specified ; null if none is found. /// - public RestRole GetRole(uint id) - { - if (_roles.TryGetValue(id, out RestRole value)) return value; - - return null; - } + public RestRole? GetRole(uint id) => _roles.TryGetValue(id, out RestRole? value) ? value : null; /// /// Creates a new role with the provided name. @@ -386,7 +390,7 @@ public RestRole GetRole(uint id) /// A task that represents the asynchronous creation operation. The task result contains the newly created /// role. /// - public async Task CreateRoleAsync(string name, RequestOptions options = null) + public async Task CreateRoleAsync(string? name = null, RequestOptions? options = null) { RestRole role = await GuildHelper.CreateRoleAsync(this, Kook, name, options).ConfigureAwait(false); _roles = _roles.Add(role.Id, role); @@ -408,8 +412,8 @@ public async Task CreateRoleAsync(string name, RequestOptions options /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users found within this guild. /// - public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) - => GuildHelper.GetUsersAsync(this, Kook, KookConfig.MaxUsersPerBatch, 1, options); + public IAsyncEnumerable> GetUsersAsync(RequestOptions? options = null) => + GuildHelper.GetUsersAsync(this, Kook, KookConfig.MaxUsersPerBatch, 1, options); /// /// Gets a user from this guild. @@ -423,8 +427,8 @@ public IAsyncEnumerable> GetUsersAsync(Reques /// A task that represents the asynchronous get operation. The task result contains the guild user /// associated with the specified ; null if none is found. /// - public Task GetUserAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Kook, id, options); + public Task GetUserAsync(ulong id, RequestOptions? options = null) => + GuildHelper.GetUserAsync(this, Kook, id, options); /// /// Gets the current user for this guild. @@ -434,8 +438,12 @@ public Task GetUserAsync(ulong id, RequestOptions options = null) /// A task that represents the asynchronous get operation. The task result contains the currently logged-in /// user within this guild. /// - public Task GetCurrentUserAsync(RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Kook, Kook.CurrentUser.Id, options); + public Task GetCurrentUserAsync(RequestOptions? options = null) + { + if (Kook.CurrentUser is null) + throw new InvalidOperationException("The current user is not set well via login."); + return GuildHelper.GetUserAsync(this, Kook, Kook.CurrentUser.Id, options); + } /// /// Gets the owner of this guild. @@ -444,8 +452,8 @@ public Task GetCurrentUserAsync(RequestOptions options = null) /// /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. /// - public Task GetOwnerAsync(RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Kook, OwnerId, options); + public Task GetOwnerAsync(RequestOptions? options = null) => + GuildHelper.GetUserAsync(this, Kook, OwnerId, options); /// /// Gets a collection of users in this guild that the name or nickname contains the @@ -461,9 +469,10 @@ public Task GetOwnerAsync(RequestOptions options = null) /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users that matches the properties with the provided at . /// - public IAsyncEnumerable> SearchUsersAsync(Action func, - int limit = KookConfig.MaxUsersPerBatch, RequestOptions options = null) - => GuildHelper.SearchUsersAsync(this, Kook, func, limit, options); + public IAsyncEnumerable> SearchUsersAsync( + Action func, int limit = KookConfig.MaxUsersPerBatch, + RequestOptions? options = null) => + GuildHelper.SearchUsersAsync(this, Kook, func, limit, options); #endregion @@ -477,8 +486,8 @@ public IAsyncEnumerable> SearchUsersAsync(Act /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// generic channels found within this guild. /// - public Task> GetChannelsAsync(RequestOptions options = null) - => GuildHelper.GetChannelsAsync(this, Kook, options); + public Task> GetChannelsAsync(RequestOptions? options = null) => + GuildHelper.GetChannelsAsync(this, Kook, options); /// /// Gets a channel in this guild. @@ -489,8 +498,8 @@ public Task> GetChannelsAsync(RequestOptio /// A task that represents the asynchronous get operation. The task result contains the generic channel /// associated with the specified ; null if none is found. /// - public Task GetChannelAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetChannelAsync(this, Kook, id, options); + public Task GetChannelAsync(ulong id, RequestOptions? options = null) => + GuildHelper.GetChannelAsync(this, Kook, id, options); /// /// Gets a text channel in this guild. @@ -501,9 +510,11 @@ public Task GetChannelAsync(ulong id, RequestOptions options = /// A task that represents the asynchronous get operation. The task result contains the text channel /// associated with the specified ; null if none is found. /// - public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) + public async Task GetTextChannelAsync(ulong id, RequestOptions? options = null) { - RestGuildChannel channel = await GuildHelper.GetChannelAsync(this, Kook, id, options).ConfigureAwait(false); + RestGuildChannel channel = await GuildHelper + .GetChannelAsync(this, Kook, id, options) + .ConfigureAwait(false); return channel as RestTextChannel; } @@ -515,10 +526,12 @@ public async Task GetTextChannelAsync(ulong id, RequestOptions /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// message channels found within this guild. /// - public async Task> GetTextChannelsAsync(RequestOptions options = null) + public async Task> GetTextChannelsAsync(RequestOptions? options = null) { - IReadOnlyCollection channels = await GuildHelper.GetChannelsAsync(this, Kook, options).ConfigureAwait(false); - return channels.OfType().ToImmutableArray(); + IReadOnlyCollection channels = await GuildHelper + .GetChannelsAsync(this, Kook, options) + .ConfigureAwait(false); + return [..channels.OfType()]; } /// @@ -530,9 +543,11 @@ public async Task> GetTextChannelsAsync(Req /// A task that represents the asynchronous get operation. The task result contains the voice channel associated /// with the specified ; null if none is found. /// - public async Task GetVoiceChannelAsync(ulong id, RequestOptions options = null) + public async Task GetVoiceChannelAsync(ulong id, RequestOptions? options = null) { - RestGuildChannel channel = await GuildHelper.GetChannelAsync(this, Kook, id, options).ConfigureAwait(false); + RestGuildChannel channel = await GuildHelper + .GetChannelAsync(this, Kook, id, options) + .ConfigureAwait(false); return channel as RestVoiceChannel; } @@ -544,10 +559,12 @@ public async Task GetVoiceChannelAsync(ulong id, RequestOption /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// voice channels found within this guild. /// - public async Task> GetVoiceChannelsAsync(RequestOptions options = null) + public async Task> GetVoiceChannelsAsync(RequestOptions? options = null) { - IReadOnlyCollection channels = await GuildHelper.GetChannelsAsync(this, Kook, options).ConfigureAwait(false); - return channels.OfType().ToImmutableArray(); + IReadOnlyCollection channels = await GuildHelper + .GetChannelsAsync(this, Kook, options) + .ConfigureAwait(false); + return [..channels.OfType()]; } /// @@ -559,9 +576,11 @@ public async Task> GetVoiceChannelsAsync(R /// A task that represents the asynchronous get operation. The task result contains the category channel associated /// with the specified ; null if none is found. /// - public async Task GetCategoryChannelAsync(ulong id, RequestOptions options = null) + public async Task GetCategoryChannelAsync(ulong id, RequestOptions? options = null) { - RestGuildChannel channel = await GuildHelper.GetChannelAsync(this, Kook, id, options).ConfigureAwait(false); + RestGuildChannel channel = await GuildHelper + .GetChannelAsync(this, Kook, id, options) + .ConfigureAwait(false); return channel as RestCategoryChannel; } @@ -573,10 +592,12 @@ public async Task GetCategoryChannelAsync(ulong id, Request /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// category channels found within this guild. /// - public async Task> GetCategoryChannelsAsync(RequestOptions options = null) + public async Task> GetCategoryChannelsAsync(RequestOptions? options = null) { - IReadOnlyCollection channels = await GuildHelper.GetChannelsAsync(this, Kook, options).ConfigureAwait(false); - return channels.OfType().ToImmutableArray(); + IReadOnlyCollection channels = await GuildHelper + .GetChannelsAsync(this, Kook, options) + .ConfigureAwait(false); + return [..channels.OfType()]; } /// @@ -587,27 +608,15 @@ public async Task> GetCategoryChannelsA /// A task that represents the asynchronous get operation. The task result contains the default text channel of this guild; /// null if none is found. /// - public async Task GetDefaultChannelAsync(RequestOptions options = null) + public async Task GetDefaultChannelAsync(RequestOptions? options = null) { - ulong? welcomeChannelId = DefaultChannelId; - if (welcomeChannelId.HasValue) - { - RestGuildChannel channel = await GuildHelper.GetChannelAsync(this, Kook, welcomeChannelId.Value, options).ConfigureAwait(false); - return channel as RestTextChannel; - } - - return null; + if (!DefaultChannelId.HasValue) return null; + RestGuildChannel channel = await GuildHelper + .GetChannelAsync(this, Kook, DefaultChannelId.Value, options) + .ConfigureAwait(false); + return channel as RestTextChannel; } - // Get first channel - // public async Task GetDefaultChannelAsync(RequestOptions options = null) - // { - // var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); - // var user = await GetCurrentUserAsync(options).ConfigureAwait(false); - // return channels - // .Where(c => user.GetPermissions(c).ViewChannel) - // .MinBy(c => c.Position); - // } /// /// Gets the welcome text channel in this guild. /// @@ -616,16 +625,13 @@ public async Task GetDefaultChannelAsync(RequestOptions options /// A task that represents the asynchronous get operation. The task result contains the welcome text channel of this guild; /// null if none is found. /// - public async Task GetWelcomeChannelAsync(RequestOptions options = null) + public async Task GetWelcomeChannelAsync(RequestOptions? options = null) { - ulong? welcomeChannelId = WelcomeChannelId; - if (welcomeChannelId.HasValue) - { - RestGuildChannel channel = await GuildHelper.GetChannelAsync(this, Kook, welcomeChannelId.Value, options).ConfigureAwait(false); - return channel as RestTextChannel; - } - - return null; + if (!WelcomeChannelId.HasValue) return null; + RestGuildChannel channel = await GuildHelper + .GetChannelAsync(this, Kook, WelcomeChannelId.Value, options) + .ConfigureAwait(false); + return channel as RestTextChannel; } /// @@ -638,8 +644,9 @@ public async Task GetWelcomeChannelAsync(RequestOptions options /// A task that represents the asynchronous creation operation. The task result contains the newly created /// text channel. /// - public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) - => GuildHelper.CreateTextChannelAsync(this, Kook, name, options, func); + public Task CreateTextChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateTextChannelAsync(this, Kook, name, func, options); /// /// Creates a voice channel with the provided name. @@ -651,9 +658,9 @@ public Task CreateTextChannelAsync(string name, Action /// The created voice channel. /// - public Task CreateVoiceChannelAsync(string name, Action func = null, - RequestOptions options = null) - => GuildHelper.CreateVoiceChannelAsync(this, Kook, name, options, func); + public Task CreateVoiceChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateVoiceChannelAsync(this, Kook, name, func, options); /// /// Creates a category channel with the provided name. @@ -665,66 +672,66 @@ public Task CreateVoiceChannelAsync(string name, Action /// The created category channel. /// - public Task CreateCategoryChannelAsync(string name, Action func = null, - RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Kook, name, options, func); + public Task CreateCategoryChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateCategoryChannelAsync(this, Kook, name, func, options); #endregion #region Voices /// - public async Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, RequestOptions options = null) - => await ClientHelper.MoveUsersAsync(Kook, users, targetChannel, options).ConfigureAwait(false); + public Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, + RequestOptions? options = null) => + ClientHelper.MoveUsersAsync(Kook, users, targetChannel, options); #endregion #region Emotes /// - public Task> GetEmotesAsync(RequestOptions options = null) - => GuildHelper.GetEmotesAsync(this, Kook, options); + public Task> GetEmotesAsync(RequestOptions? options = null) => + GuildHelper.GetEmotesAsync(this, Kook, options); /// - public Task GetEmoteAsync(string id, RequestOptions options = null) - => GuildHelper.GetEmoteAsync(this, Kook, id, options); + public Task GetEmoteAsync(string id, RequestOptions? options = null) => + GuildHelper.GetEmoteAsync(this, Kook, id, options); /// - public Task CreateEmoteAsync(string name, Image image, RequestOptions options = null) - => GuildHelper.CreateEmoteAsync(this, Kook, name, image, options); + public Task CreateEmoteAsync(string name, Image image, RequestOptions? options = null) => + GuildHelper.CreateEmoteAsync(this, Kook, name, image, options); /// /// is null. - public Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions options = null) - => GuildHelper.ModifyEmoteNameAsync(this, Kook, emote, name, options); + public Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions? options = null) => + GuildHelper.ModifyEmoteNameAsync(this, Kook, emote, name, options); /// - public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) - => GuildHelper.DeleteEmoteAsync(this, Kook, emote.Id, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions? options = null) => + GuildHelper.DeleteEmoteAsync(this, Kook, emote.Id, options); #endregion #region Invites /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await GuildHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); + public async Task> GetInvitesAsync(RequestOptions? options = null) => + await GuildHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null) - => await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + async Task IGuild.CreateInviteAsync(int? maxAge, int? maxUses, RequestOptions? options) => + await CreateInviteAsync(maxAge, maxUses, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) - => await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + async Task IGuild.CreateInviteAsync(InviteMaxAge maxAge, InviteMaxUses maxUses, RequestOptions? options) => + await CreateInviteAsync(maxAge, maxUses, options).ConfigureAwait(false); #endregion #region IGuild /// - IAudioClient IGuild.AudioClient => null; + IAudioClient? IGuild.AudioClient => null; /// bool IGuild.Available => Available; @@ -736,187 +743,147 @@ public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge. IReadOnlyCollection IGuild.Emotes => Emotes; /// - IRecommendInfo IGuild.RecommendInfo => RecommendInfo; + IRecommendInfo? IGuild.RecommendInfo => RecommendInfo; /// IRole IGuild.EveryoneRole => EveryoneRole; /// - IRole IGuild.GetRole(uint id) => GetRole(id); + IRole? IGuild.GetRole(uint id) => GetRole(id); /// - async Task IGuild.CreateRoleAsync(string name, RequestOptions options) - => await CreateRoleAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateRoleAsync(string name, RequestOptions? options) => + await CreateRoleAsync(name, options).ConfigureAwait(false); /// - async Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetCurrentUserAsync(options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetCurrentUserAsync(options).ConfigureAwait(false) + : null; /// - async Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetOwnerAsync(options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetOwnerAsync(options).ConfigureAwait(false) + : null; /// - async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); - else - return ImmutableArray.Create(); - } + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? [..await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)] + : []; /// /// Downloading users is not supported for a REST-based guild. - Task IGuild.DownloadUsersAsync(RequestOptions options) => - throw new NotSupportedException(); + Task IGuild.DownloadUsersAsync(RequestOptions? options) => throw new NotSupportedException(); /// /// Downloading voice states is not supported for a REST-based guild. - Task IGuild.DownloadVoiceStatesAsync(RequestOptions options) => - throw new NotSupportedException(); + Task IGuild.DownloadVoiceStatesAsync(RequestOptions? options) => throw new NotSupportedException(); /// /// Downloading boost subscriptions is not supported for a REST-based guild. - Task IGuild.DownloadBoostSubscriptionsAsync(RequestOptions options) => - throw new NotSupportedException(); + Task IGuild.DownloadBoostSubscriptionsAsync(RequestOptions? options) => throw new NotSupportedException(); /// - IAsyncEnumerable> IGuild.SearchUsersAsync(Action func, int limit, CacheMode mode, - RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return SearchUsersAsync(func, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IGuild.SearchUsersAsync(Action func, + int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? SearchUsersAsync(func, limit, options) + : AsyncEnumerable.Empty>(); /// - async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetUserAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetUserAsync(id, options).ConfigureAwait(false) + : null; /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + async Task> IGuild.GetBansAsync(RequestOptions? options) => + await GetBansAsync(options).ConfigureAwait(false); /// - async Task IGuild.GetBanAsync(IUser user, RequestOptions options) - => await GetBanAsync(user, options).ConfigureAwait(false); + async Task IGuild.GetBanAsync(IUser user, RequestOptions? options) => + await GetBanAsync(user, options).ConfigureAwait(false); /// - async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) - => await GetBanAsync(userId, options).ConfigureAwait(false); + async Task IGuild.GetBanAsync(ulong userId, RequestOptions? options) => + await GetBanAsync(userId, options).ConfigureAwait(false); /// - async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetChannelsAsync(options).ConfigureAwait(false); - else - return ImmutableArray.Create(); - } + async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetChannelsAsync(options).ConfigureAwait(false) + : Channels; /// - async Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetChannelAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetChannelAsync(id, options).ConfigureAwait(false) + : Channels.FirstOrDefault(x => x.Id == id); /// - async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetDefaultChannelAsync(options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetDefaultChannelAsync(options).ConfigureAwait(false) + : Channels.FirstOrDefault(x => x.Id == DefaultChannelId) as ITextChannel; /// - async Task IGuild.GetWelcomeChannelAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetWelcomeChannelAsync(options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetWelcomeChannelAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetWelcomeChannelAsync(options).ConfigureAwait(false) + : Channels.FirstOrDefault(x => x.Id == WelcomeChannelId) as ITextChannel; /// - async Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetTextChannelsAsync(options).ConfigureAwait(false); - else - return ImmutableArray.Create(); - } + async Task> IGuild.GetTextChannelsAsync(CacheMode mode, + RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetTextChannelsAsync(options).ConfigureAwait(false) + : TextChannels; /// - async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetTextChannelAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetTextChannelAsync(id, options).ConfigureAwait(false) + : TextChannels.FirstOrDefault(x => x.Id == id); /// - async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetVoiceChannelsAsync(options).ConfigureAwait(false); - else - return ImmutableArray.Create(); - } + async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, + RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetVoiceChannelsAsync(options).ConfigureAwait(false) + : VoiceChannels; /// - async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetVoiceChannelAsync(id, options).ConfigureAwait(false); - else - return null; - } + async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetVoiceChannelAsync(id, options).ConfigureAwait(false) + : VoiceChannels.FirstOrDefault(x => x.Id == id); /// async Task> IGuild.GetCategoryChannelsAsync(CacheMode mode, - RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetCategoryChannelsAsync(options).ConfigureAwait(false); - else - return null; - } + RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetCategoryChannelsAsync(options).ConfigureAwait(false) + : CategoryChannels; /// - async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) - => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateTextChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) - => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateCategoryChannelAsync(string name, Action func, RequestOptions options) - => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); /// - public async Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions options = null) => + public async Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions? options = null) => await GuildHelper.GetBadgeAsync(this, Kook, style, options).ConfigureAwait(false); #endregion diff --git a/src/Kook.Net.Rest/Entities/Intimacies/IntimacyHelper.cs b/src/Kook.Net.Rest/Entities/Intimacies/IntimacyHelper.cs index f5db5124..38810c2b 100644 --- a/src/Kook.Net.Rest/Entities/Intimacies/IntimacyHelper.cs +++ b/src/Kook.Net.Rest/Entities/Intimacies/IntimacyHelper.cs @@ -5,14 +5,16 @@ namespace Kook.Rest; internal static class IntimacyHelper { public static async Task UpdateAsync(IIntimacy intimacy, BaseKookClient client, - Action func, - RequestOptions options) + Action func, RequestOptions? options) { - IntimacyProperties properties = new() { Score = intimacy.Score, SocialInfo = intimacy.SocialInfo }; + IntimacyProperties properties = new(intimacy.SocialInfo, intimacy.Score); func(properties); UpdateIntimacyValueParams args = new() { - UserId = intimacy.User.Id, Score = properties.Score, SocialInfo = properties.SocialInfo, ImageId = properties.ImageId + UserId = intimacy.User.Id, + Score = properties.Score, + SocialInfo = properties.SocialInfo, + ImageId = properties.ImageId }; await client.ApiClient.UpdateIntimacyValueAsync(args, options).ConfigureAwait(false); } diff --git a/src/Kook.Net.Rest/Entities/Intimacies/RestIntimacy.cs b/src/Kook.Net.Rest/Entities/Intimacies/RestIntimacy.cs index 66aad9dd..a72d0d1e 100644 --- a/src/Kook.Net.Rest/Entities/Intimacies/RestIntimacy.cs +++ b/src/Kook.Net.Rest/Entities/Intimacies/RestIntimacy.cs @@ -20,7 +20,7 @@ public class RestIntimacy : RestEntity, IIntimacy public DateTimeOffset LastReadAt { get; internal set; } /// - public DateTimeOffset LastModifyAt { get; internal set; } + public DateTimeOffset? LastModifyAt { get; internal set; } /// public int Score { get; internal set; } @@ -29,12 +29,16 @@ public class RestIntimacy : RestEntity, IIntimacy public IReadOnlyCollection Images => _images; /// - public async Task UpdateAsync(Action func, RequestOptions options = null) => + public async Task UpdateAsync(Action func, RequestOptions? options = null) => await IntimacyHelper.UpdateAsync(this, Kook, func, options).ConfigureAwait(false); internal RestIntimacy(BaseKookClient kook, IUser user, ulong id) - : base(kook, id) => + : base(kook, id) + { + _images = []; User = user; + SocialInfo = string.Empty; + } internal static RestIntimacy Create(BaseKookClient kook, IUser user, Model model) { @@ -50,6 +54,6 @@ internal void Update(Model model) LastReadAt = model.LastReadAt; LastModifyAt = model.LastModifyAt; Score = model.Score; - _images = model.Images.Select(i => new IntimacyImage(i.Id, i.Url)).ToImmutableArray(); + _images = [..model.Images.Select(i => new IntimacyImage(i.Id, i.Url))]; } } diff --git a/src/Kook.Net.Rest/Entities/Invites/InviteHelper.cs b/src/Kook.Net.Rest/Entities/Invites/InviteHelper.cs index e72afe0a..9baaa118 100644 --- a/src/Kook.Net.Rest/Entities/Invites/InviteHelper.cs +++ b/src/Kook.Net.Rest/Entities/Invites/InviteHelper.cs @@ -4,10 +4,14 @@ namespace Kook.Rest; internal static class InviteHelper { - public static async Task DeleteAsync(IInvite invite, BaseKookClient client, - RequestOptions options) + public static async Task DeleteAsync(IInvite invite, BaseKookClient client, RequestOptions? options) { - DeleteGuildInviteParams args = new() { GuildId = invite.GuildId, ChannelId = invite.ChannelId, UrlCode = invite.Code }; + DeleteGuildInviteParams args = new() + { + GuildId = invite.GuildId, + ChannelId = invite.ChannelId, + UrlCode = invite.Code + }; await client.ApiClient.DeleteGuildInviteAsync(args, options).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Rest/Entities/Invites/RestInvite.cs b/src/Kook.Net.Rest/Entities/Invites/RestInvite.cs index f869a3c3..30b5a359 100644 --- a/src/Kook.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Kook.Net.Rest/Entities/Invites/RestInvite.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Model = Kook.API.Invite; namespace Kook.Rest; @@ -6,7 +7,7 @@ namespace Kook.Rest; /// /// Represents a REST-based invite. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestInvite : RestEntity, IInvite, IUpdateable { /// @@ -25,7 +26,7 @@ public class RestInvite : RestEntity, IInvite, IUpdateable public ulong? ChannelId { get; private set; } /// - public string ChannelName { get; private set; } + public string? ChannelName { get; private set; } /// public ulong? GuildId { get; private set; } @@ -33,6 +34,9 @@ public class RestInvite : RestEntity, IInvite, IUpdateable /// public string GuildName { get; private set; } + /// + public DateTimeOffset CreatedAt { get; private set; } + /// public DateTimeOffset? ExpiresAt { get; private set; } @@ -48,51 +52,61 @@ public class RestInvite : RestEntity, IInvite, IUpdateable /// public int? RemainingUses { get; private set; } + /// + public int InvitedUsersCount { get; private set; } + internal IChannel Channel { get; } internal IGuild Guild { get; } - internal RestInvite(BaseKookClient kook, IGuild guild, IChannel channel, uint id) - : base(kook, id) + internal RestInvite(BaseKookClient kook, IGuild guild, IChannel channel, Model model) + : base(kook, model.Id) { Guild = guild; Channel = channel; + Update(model); } - internal static RestInvite Create(BaseKookClient kook, IGuild guild, IChannel channel, Model model) - { - RestInvite entity = new(kook, guild, channel, model.Id); - entity.Update(model); - return entity; - } + internal static RestInvite Create(BaseKookClient kook, IGuild guild, IChannel channel, Model model) => + new(kook, guild, channel, model); + [MemberNotNull( + nameof(Code), + nameof(Url), + nameof(Inviter), + nameof(GuildName))] internal void Update(Model model) { Code = model.UrlCode; Url = model.Url; GuildId = model.GuildId; - ChannelId = model.ChannelId; + ChannelId = model.ChannelId != 0 ? model.ChannelId : null; GuildName = model.GuildName; - ChannelName = model.ChannelName; + ChannelName = model.ChannelId != 0 ? model.ChannelName : null; ChannelType = model.ChannelType == ChannelType.Category ? ChannelType.Unspecified : model.ChannelType; - Inviter = model.Inviter is not null ? RestUser.Create(Kook, model.Inviter) : null; + Inviter = RestUser.Create(Kook, model.Inviter); + CreatedAt = model.CreatedAt; ExpiresAt = model.ExpiresAt; MaxAge = model.Duration; MaxUses = model.UsingTimes == -1 ? null : model.UsingTimes; RemainingUses = model.RemainingTimes == -1 ? null : model.RemainingTimes; Uses = MaxUses - RemainingUses; + InvitedUsersCount = model.InviteesCount; } /// - public async Task UpdateAsync(RequestOptions options = null) + public async Task UpdateAsync(RequestOptions? options = null) { - IEnumerable model = - await Kook.ApiClient.GetGuildInvitesAsync(GuildId, ChannelId, options: options).FlattenAsync().ConfigureAwait(false); - Update(model.SingleOrDefault(i => i.UrlCode == Code)); + IEnumerable model = await Kook.ApiClient + .GetGuildInvitesAsync(GuildId, ChannelId, options: options) + .FlattenAsync().ConfigureAwait(false); + if (model.SingleOrDefault(i => i.UrlCode == Code) is not { } updateModel) + throw new InvalidOperationException("Cannot fetch the invite from the API."); + Update(updateModel); } /// - public Task DeleteAsync(RequestOptions options = null) - => InviteHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + InviteHelper.DeleteAsync(this, Kook, options); /// /// Gets the URL of the invite. @@ -105,26 +119,8 @@ public Task DeleteAsync(RequestOptions options = null) private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName ?? "Channel not specified"})"; /// - IGuild IInvite.Guild - { - get - { - if (Guild != null) return Guild; - - if (Channel is IGuildChannel guildChannel) return guildChannel.Guild; //If it fails, it'll still return this exception - - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } + IGuild IInvite.Guild => Guild; /// - IChannel IInvite.Channel - { - get - { - if (Channel != null) return Channel; - - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } + IChannel IInvite.Channel => Channel; } diff --git a/src/Kook.Net.Rest/Entities/Messages/Attachment.cs b/src/Kook.Net.Rest/Entities/Messages/Attachment.cs index 72376997..ce32d455 100644 --- a/src/Kook.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Kook.Net.Rest/Entities/Messages/Attachment.cs @@ -4,7 +4,7 @@ namespace Kook; /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class Attachment : IAttachment { /// @@ -14,13 +14,13 @@ public class Attachment : IAttachment public string Url { get; } /// - public string Filename { get; } + public string? Filename { get; } /// public int? Size { get; } /// - public string FileType { get; } + public string? FileType { get; } /// public TimeSpan? Duration { get; } @@ -31,7 +31,8 @@ public class Attachment : IAttachment /// public int? Height { get; } - internal Attachment(AttachmentType type, string url, string filename, int? size, string fileType, TimeSpan? duration, int? width, int? height) + internal Attachment(AttachmentType type, string url, string? filename, + int? size = null, string? fileType = null, TimeSpan? duration = null, int? width = null, int? height = null) { Type = type; Url = url; @@ -55,7 +56,8 @@ internal static Attachment Create(Model model) TimeSpan? duration = model.Duration.HasValue ? TimeSpan.FromSeconds(model.Duration.Value) : null; - return new Attachment(type, model.Url, model.Name, model.Size, model.FileType, duration, model.Width, model.Height); + return new Attachment(type, model.Url, model.Name, + model.Size, model.FileType, duration, model.Width, model.Height); } /// @@ -64,7 +66,7 @@ internal static Attachment Create(Model model) /// /// A string containing the filename of this attachment. /// - public override string ToString() => Filename; + public override string? ToString() => Filename; private string DebuggerDisplay => $"{Filename}{(Size.HasValue ? $" ({Size} bytes)" : "")}"; } diff --git a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs index b43fa08f..f7af639c 100644 --- a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs @@ -17,13 +17,15 @@ internal static class MessageHelper /// Regex used to check if some text is formatted as inline code. /// private static readonly Regex InlineCodeRegex = - new(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + new(@"[^\\]?(`).+?[^\\](`)", + RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); /// /// Regex used to check if some text is formatted as a code block. /// private static readonly Regex BlockCodeRegex = - new(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + new(@"[^\\]?(```).+?[^\\](```)", + RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); private static Regex PlainTextUserRegex => MentionUtils.PlainTextUserRegex; private static Regex PlainTextRoleRegex => MentionUtils.PlainTextRoleRegex; @@ -36,158 +38,222 @@ internal static class MessageHelper private static Regex KMarkdownEmojiRegex => Emote.KMarkdownEmojiRegex; private static Regex KMarkdownTagRegex => MentionUtils.KMarkdownTagRegex; - public static Task DeleteAsync(IMessage msg, BaseKookClient client, RequestOptions options) - => DeleteAsync(msg.Id, client, options); + public static Task DeleteAsync(IMessage msg, BaseKookClient client, RequestOptions? options) => + DeleteAsync(msg.Id, client, options); - public static async Task DeleteAsync(Guid msgId, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteAsync(Guid msgId, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteMessageAsync(msgId, options).ConfigureAwait(false); - public static Task DeleteDirectAsync(IMessage msg, BaseKookClient client, RequestOptions options) - => DeleteDirectAsync(msg.Id, client, options); + public static Task DeleteDirectAsync(IMessage msg, BaseKookClient client, RequestOptions? options) => + DeleteDirectAsync(msg.Id, client, options); - public static async Task DeleteDirectAsync(Guid msgId, BaseKookClient client, - RequestOptions options) => + public static async Task DeleteDirectAsync(Guid msgId, BaseKookClient client, RequestOptions? options) => await client.ApiClient.DeleteDirectMessageAsync(msgId, options).ConfigureAwait(false); - public static async Task AddReactionAsync(Guid messageId, IEmote emote, BaseKookClient client, - RequestOptions options) + public static async Task AddReactionAsync(Guid messageId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - AddReactionParams args = new() { EmojiId = emote.Id, MessageId = messageId }; + AddReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = messageId + }; await client.ApiClient.AddReactionAsync(args, options).ConfigureAwait(false); } - public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseKookClient client, RequestOptions options) + public static async Task AddReactionAsync(IMessage msg, IEmote emote, + BaseKookClient client, RequestOptions? options) { - AddReactionParams args = new() { EmojiId = emote.Id, MessageId = msg.Id }; - + AddReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = msg.Id + }; await client.ApiClient.AddReactionAsync(args, options).ConfigureAwait(false); } - public static async Task AddDirectMessageReactionAsync(Guid messageId, IEmote emote, BaseKookClient client, - RequestOptions options) + public static async Task AddDirectMessageReactionAsync(Guid messageId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - AddReactionParams args = new() { EmojiId = emote.Id, MessageId = messageId }; + AddReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = messageId + }; await client.ApiClient.AddDirectMessageReactionAsync(args, options).ConfigureAwait(false); } - public static async Task AddDirectMessageReactionAsync(IMessage msg, IEmote emote, BaseKookClient client, RequestOptions options) + public static async Task AddDirectMessageReactionAsync(IMessage msg, IEmote emote, + BaseKookClient client, RequestOptions? options) { - AddReactionParams args = new() { EmojiId = emote.Id, MessageId = msg.Id }; - + AddReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = msg.Id + }; await client.ApiClient.AddDirectMessageReactionAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveReactionAsync(Guid messageId, ulong userId, IEmote emote, BaseKookClient client, RequestOptions options) + public static async Task RemoveReactionAsync(Guid messageId, ulong userId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - RemoveReactionParams args = new() { EmojiId = emote.Id, MessageId = messageId, UserId = userId == client.CurrentUser.Id ? null : userId }; + RemoveReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = messageId, + UserId = userId == client.CurrentUser?.Id ? null : userId + }; await client.ApiClient.RemoveReactionAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseKookClient client, RequestOptions options) + public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - RemoveReactionParams args = new() { EmojiId = emote.Id, MessageId = msg.Id, UserId = userId == client.CurrentUser.Id ? null : userId }; + RemoveReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = msg.Id, + UserId = userId == client.CurrentUser?.Id ? null : userId + }; await client.ApiClient.RemoveReactionAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveDirectMessageReactionAsync(Guid messageId, ulong userId, IEmote emote, BaseKookClient client, - RequestOptions options) + public static async Task RemoveDirectMessageReactionAsync(Guid messageId, ulong userId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - RemoveReactionParams args = new() { EmojiId = emote.Id, MessageId = messageId, UserId = userId == client.CurrentUser.Id ? null : userId }; + RemoveReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = messageId, + UserId = userId == client.CurrentUser?.Id ? null : userId + }; await client.ApiClient.RemoveDirectMessageReactionAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveDirectMessageReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseKookClient client, RequestOptions options) + public static async Task RemoveDirectMessageReactionAsync(IMessage msg, ulong userId, IEmote emote, + BaseKookClient client, RequestOptions? options) { - RemoveReactionParams args = new() { EmojiId = emote.Id, MessageId = msg.Id, UserId = userId == client.CurrentUser.Id ? null : userId }; + RemoveReactionParams args = new() + { + EmojiId = emote.Id, + MessageId = msg.Id, + UserId = userId == client.CurrentUser?.Id ? null : userId + }; await client.ApiClient.RemoveDirectMessageReactionAsync(args, options).ConfigureAwait(false); } - public static async Task> GetReactionUsersAsync(IMessage msg, IEmote emote, BaseKookClient client, - RequestOptions options) + public static async Task> GetReactionUsersAsync(IMessage msg, IEmote emote, + BaseKookClient client, RequestOptions? options) { - IReadOnlyCollection models = - await client.ApiClient.GetReactionUsersAsync(msg.Id, emote.Id, options).ConfigureAwait(false); - return models.Select(x => RestUser.Create(client, x)).ToImmutableArray(); + IReadOnlyCollection models = await client.ApiClient + .GetReactionUsersAsync(msg.Id, emote.Id, options) + .ConfigureAwait(false); + return [..models.Select(x => RestUser.Create(client, x))]; } - public static async Task> GetDirectMessageReactionUsersAsync(IMessage msg, IEmote emote, BaseKookClient client, - RequestOptions options) + public static async Task> GetDirectMessageReactionUsersAsync( + IMessage msg, IEmote emote, BaseKookClient client, RequestOptions? options) { - IReadOnlyCollection models = - await client.ApiClient.GetDirectMessageReactionUsersAsync(msg.Id, emote.Id, options).ConfigureAwait(false); - return models.Select(x => RestUser.Create(client, x)).ToImmutableArray(); + IReadOnlyCollection models = await client.ApiClient + .GetDirectMessageReactionUsersAsync(msg.Id, emote.Id, options) + .ConfigureAwait(false); + return [..models.Select(x => RestUser.Create(client, x))]; } public static async Task ModifyAsync(IUserMessage msg, BaseKookClient client, Action func, - RequestOptions options) + RequestOptions? options) { if (msg.Type == MessageType.KMarkdown) { - MessageProperties args = new() { Content = msg.Content, Quote = msg.Quote as Quote }; + MessageProperties args = new() + { + Content = msg.Content, + Quote = msg.Quote + }; func(args); Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - await ModifyAsync(msg.Id, client, args.Content, options, args.Quote, args.EphemeralUser); + await ModifyAsync(msg.Id, client, args.Content, args.Quote, args.EphemeralUser, options); return; } if (msg.Type == MessageType.Card) { - MessageProperties args = new() { Cards = msg.Cards, Quote = msg.Quote as Quote }; + MessageProperties args = new() + { + Cards = msg.Cards, + Quote = msg.Quote + }; func(args); - if (args.Cards is null || !args.Cards.Any()) throw new ArgumentNullException(nameof(args.Cards), "CardMessage must contains cards."); - + if (args.Cards is null || !args.Cards.Any()) + throw new ArgumentNullException(nameof(args.Cards), "CardMessage must contains cards."); string json = SerializeCards(args.Cards); - await ModifyAsync(msg.Id, client, json, options, args.Quote, args.EphemeralUser); + await ModifyAsync(msg.Id, client, json, args.Quote, args.EphemeralUser, options); return; } throw new NotSupportedException("Only the modification of KMarkdown and CardMessage are supported."); } - public static async Task ModifyAsync(Guid msgId, BaseKookClient client, Action func, - RequestOptions options, IQuote quote = null, IUser ephemeralUser = null) + public static async Task ModifyAsync(Guid msgId, BaseKookClient client, + Action func, RequestOptions? options) { MessageProperties properties = new(); func(properties); if (string.IsNullOrEmpty(properties.Content) ^ (properties.Cards is not null && properties.Cards.Any())) throw new InvalidOperationException("Only one of arguments can be set between Content and Cards"); - string content = string.Empty; - if (!string.IsNullOrEmpty(properties.Content)) content = properties.Content; - - if (properties.Cards is not null && properties.Cards.Any()) content = SerializeCards(properties.Cards); + string content; + if (properties.Content != null && !string.IsNullOrEmpty(properties.Content)) + content = properties.Content; + else if (properties.Cards is not null && properties.Cards.Any()) + content = SerializeCards(properties.Cards); + else + content = string.Empty; - await ModifyAsync(msgId, client, content, options, quote, ephemeralUser); + await ModifyAsync(msgId, client, content, properties.Quote, properties.EphemeralUser, options); } public static async Task ModifyAsync(Guid msgId, BaseKookClient client, string content, - RequestOptions options, IQuote quote = null, IUser ephemeralUser = null) + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) { - ModifyMessageParams args = new(msgId, content) { QuotedMessageId = quote?.QuotedMessageId, EphemeralUserId = ephemeralUser?.Id }; + ModifyMessageParams args = new() + { + MessageId = msgId, + Content = content, + QuotedMessageId = quote?.QuotedMessageId, + EphemeralUserId = ephemeralUser?.Id + }; await client.ApiClient.ModifyMessageAsync(args, options).ConfigureAwait(false); } - public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, Action func, - RequestOptions options, IQuote quote = null) + public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, + Action func, RequestOptions? options) { MessageProperties properties = new(); func(properties); if (string.IsNullOrEmpty(properties.Content) ^ (properties.Cards is not null && properties.Cards.Any())) throw new InvalidOperationException("Only one of arguments can be set between Content and Cards"); - string content = string.Empty; - if (!string.IsNullOrEmpty(properties.Content)) content = properties.Content; + string content; + if (properties.Content != null && !string.IsNullOrEmpty(properties.Content)) + content = properties.Content; + else if (properties.Cards is not null && properties.Cards.Any()) + content = SerializeCards(properties.Cards); + else + content = string.Empty; - if (properties.Cards is not null && properties.Cards.Any()) content = SerializeCards(properties.Cards); - - await ModifyDirectAsync(msgId, client, content, options, quote); + await ModifyDirectAsync(msgId, client, content, properties.Quote, options); } - public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, string content, - RequestOptions options, IQuote quote = null) + public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, + string content, IQuote? quote, RequestOptions? options) { - ModifyDirectMessageParams args = new(msgId, content) { QuotedMessageId = quote?.QuotedMessageId }; + ModifyDirectMessageParams args = new() + { + MessageId = msgId, + Content = content, + QuotedMessageId = quote?.QuotedMessageId + }; await client.ApiClient.ModifyDirectMessageAsync(args, options).ConfigureAwait(false); } @@ -197,14 +263,12 @@ public static ImmutableArray ParseCards(string json) { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { new CardConverter(), new ModuleConverter(), new ElementConverter() } + Converters = { CardConverterFactory.Instance } }; - CardBase[] cardBases = JsonSerializer.Deserialize(json, serializerOptions); - - ImmutableArray.Builder cards = ImmutableArray.CreateBuilder(cardBases.Length); - foreach (CardBase cardBase in cardBases) cards.Add(cardBase.ToEntity()); - - return cards.ToImmutable(); + CardBase[]? cardBases = JsonSerializer.Deserialize(json, serializerOptions); + if (cardBases is null) + throw new InvalidOperationException("Failed to parse cards from the provided JSON."); + return [..cardBases.Select(x => x.ToEntity())]; } public static string SerializeCards(IEnumerable cards) @@ -212,54 +276,53 @@ public static string SerializeCards(IEnumerable cards) const int maxModuleCount = 50; IEnumerable enumerable = cards as ICard[] ?? cards.ToArray(); Preconditions.AtMost(enumerable.Sum(c => c.ModuleCount), maxModuleCount, nameof(cards), - $"A max of {maxModuleCount} modules are allowed."); + $"A max of {maxModuleCount} modules can be included in a card."); - CardBase[] cardBases = enumerable.Select(c => c.ToModel()).ToArray(); JsonSerializerOptions serializerOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { new CardConverter(), new ModuleConverter(), new ElementConverter() }, + Converters = { CardConverterFactory.Instance }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - return JsonSerializer.Serialize(cardBases, serializerOptions); + return JsonSerializer.Serialize(enumerable.Select(c => c.ToModel()), serializerOptions); } public static IReadOnlyCollection ParseAttachments(IEnumerable cards) { - List attachments = new(); + List attachments = []; IEnumerable modules = cards.SelectMany(x => x.Modules); foreach (IModule module in modules) { switch (module) { case FileModule fileModule: - attachments.Add(new Attachment(AttachmentType.File, fileModule.Source, fileModule.Title, - null, null, null, null, null)); + Attachment file = new(AttachmentType.File, fileModule.Source, fileModule.Title); + attachments.Add(file); break; case AudioModule audioModule: - attachments.Add(new Attachment(AttachmentType.Audio, audioModule.Source, audioModule.Title, - null, null, null, null, null)); + Attachment audio = new(AttachmentType.Audio, audioModule.Source, audioModule.Title); + attachments.Add(audio); break; case VideoModule videoModule: - attachments.Add(new Attachment(AttachmentType.Video, videoModule.Source, videoModule.Title, - null, null, null, null, null)); + Attachment video = new(AttachmentType.Video, videoModule.Source, videoModule.Title); + attachments.Add(video); break; case ContainerModule containerModule: - attachments.AddRange(containerModule.Elements.Select(x => - new Attachment(AttachmentType.Image, x.Source, x.Alternative, - null, null, null, null, null))); + IEnumerable containerImages = containerModule.Elements + .Select(x => new Attachment(AttachmentType.Image, x.Source, x.Alternative)); + attachments.AddRange(containerImages); break; case ImageGroupModule imageGroupModule: - attachments.AddRange(imageGroupModule.Elements.Select(x => - new Attachment(AttachmentType.Image, x.Source, x.Alternative, - null, null, null, null, null))); + IEnumerable groupImages = imageGroupModule.Elements + .Select(x => new Attachment(AttachmentType.Image, x.Source, x.Alternative)); + attachments.AddRange(groupImages); break; case ContextModule contextModule: - attachments.AddRange(contextModule.Elements + IEnumerable contextImages = contextModule.Elements .OfType() - .Select(x => new Attachment(AttachmentType.Image, x.Source, x.Alternative, - null, null, null, null, null))); + .Select(x => new Attachment(AttachmentType.Image, x.Source, x.Alternative)); + attachments.AddRange(contextImages); break; } } @@ -269,62 +332,19 @@ public static IReadOnlyCollection ParseAttachments(IEnumerable public static string SanitizeMessage(IMessage message) { - string newContent = MentionUtils.Resolve(message, 0, TagHandling.FullName, TagHandling.FullName, - TagHandling.FullName, TagHandling.FullName, TagHandling.FullName); - newContent = Format.StripMarkDown(newContent); - return newContent; + string newContent = MentionUtils.Resolve(message, 0, + TagHandling.FullName, TagHandling.FullName, TagHandling.FullName, + TagHandling.FullName, TagHandling.FullName); + return newContent.StripMarkDown(); } - public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, + public static ImmutableArray ParseTags(string text, IMessageChannel? channel, IGuild? guild, IReadOnlyCollection userMentions, TagMode tagMode) { ImmutableArray.Builder tags = ImmutableArray.CreateBuilder(); - int index = 0; + int index; int codeIndex = 0; - // checks if the tag being parsed is wrapped in code blocks - bool CheckWrappedCode() - { - // util to check if the index of a tag is within the bounds of the codeblock - bool EnclosedInBlock(Match m) - => m.Groups[1].Index < index && index < m.Groups[2].Index; - - // PlainText mode - if (tagMode == TagMode.PlainText) return false; - - // loop through all code blocks that are before the start of the tag - while (codeIndex < index) - { - Match blockMatch = BlockCodeRegex.Match(text, codeIndex); - if (blockMatch.Success) - { - if (EnclosedInBlock(blockMatch)) return true; - - // continue if the end of the current code was before the start of the tag - codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length; - if (codeIndex < index) continue; - - return false; - } - - Match inlineMatch = InlineCodeRegex.Match(text, codeIndex); - if (inlineMatch.Success) - { - if (EnclosedInBlock(inlineMatch)) return true; - - // continue if the end of the current code was before the start of the tag - codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length; - if (codeIndex < index) continue; - - return false; - } - - return false; - } - - return false; - } - MatchCollection matchCollection = tagMode switch { TagMode.PlainText => PlainTextTagRegex.Matches(text), @@ -339,114 +359,149 @@ bool EnclosedInBlock(Match m) #endif { index = match.Index; - if (CheckWrappedCode()) break; + if (CheckWrappedCode()) + break; string content = match.Groups[0].Value; - if (MentionUtils.TryParseUser(content, out ulong id, tagMode)) + if (MentionUtils.TryParseUser(content, out ulong userId, tagMode)) { - IUser mentionedUser = null; - foreach (IUser mention in userMentions) - { - if (mention.Id == id) - { - mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - if (mentionedUser == null) mentionedUser = mention; - - break; - } - } - - tags.Add(new Tag(TagType.UserMention, index, content.Length, id, mentionedUser)); + IUser? mentionedUser = userMentions.FirstOrDefault(x => x.Id == userId); + IUser? channelUser = channel?.GetUserAsync(userId, CacheMode.CacheOnly).GetAwaiter().GetResult(); + tags.Add(new Tag(TagType.UserMention, index, content.Length, userId, channelUser ?? mentionedUser)); } - else if (MentionUtils.TryParseChannel(content, out id, tagMode)) + else if (MentionUtils.TryParseChannel(content, out ulong channelId, tagMode)) { - IChannel mentionedChannel = null; - if (guild != null) mentionedChannel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - - tags.Add(new Tag(TagType.ChannelMention, index, content.Length, id, mentionedChannel)); + IGuildChannel? mentionedChannel = guild?.GetChannelAsync(channelId, CacheMode.CacheOnly).GetAwaiter().GetResult(); + tags.Add(new Tag(TagType.ChannelMention, index, content.Length, channelId, mentionedChannel)); } else if (MentionUtils.TryParseRole(content, out uint roleId, tagMode)) { - IRole mentionedRole = null; - if (guild != null) mentionedRole = guild.GetRole(roleId); - + IRole? mentionedRole = guild?.GetRole(roleId); tags.Add(new Tag(TagType.RoleMention, index, content.Length, roleId, mentionedRole)); } - else if (Emote.TryParse(content, out Emote emoji, tagMode)) + else if (Emote.TryParse(content, out Emote? emoji, tagMode)) tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id, emoji)); - else //Bad Tag - continue; + // Bad Tag } index = 0; codeIndex = 0; - while (true) + if (tagMode == TagMode.PlainText) { - index = text.IndexOf("@全体成员", index, StringComparison.Ordinal); - if (index == -1) break; - - if (CheckWrappedCode()) break; - - int? tagIndex = FindIndex(tags, index); - if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, - new Tag(TagType.EveryoneMention, index, "@全体成员".Length, 0, guild?.EveryoneRole)); - - index++; + while (true) + { + index = text.IndexOf("@全体成员", index, StringComparison.Ordinal); + if (index == -1) break; + if (CheckWrappedCode()) break; + int? tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + { + Tag everyoneMention = new(TagType.EveryoneMention, index, "@全体成员".Length, 0, guild?.EveryoneRole); + tags.Insert(tagIndex.Value, everyoneMention); + } + index++; + } + } + else + { + while (true) + { + index = text.IndexOf("(met)all(met)", index, StringComparison.Ordinal); + if (index == -1) break; + if (CheckWrappedCode()) break; + int? tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + { + Tag everyoneMention = new(TagType.EveryoneMention, index, "(met)all(met)".Length, 0, guild?.EveryoneRole); + tags.Insert(tagIndex.Value, everyoneMention); + } + index++; + } } index = 0; codeIndex = 0; - while (true) + if (tagMode == TagMode.PlainText) { - index = text.IndexOf("(met)all(met)", index, StringComparison.Ordinal); - if (index == -1) break; + while (true) + { + index = text.IndexOf("@在线成员", index, StringComparison.Ordinal); + if (index == -1) break; + if (CheckWrappedCode()) break; + int? tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + { + Tag hereMention = new(TagType.HereMention, index, "@在线成员".Length, 0, null); + tags.Insert(tagIndex.Value, hereMention); + } + index++; + } + } + else + { + while (true) + { + index = text.IndexOf("(met)here(met)", index, StringComparison.Ordinal); + if (index == -1) break; + if (CheckWrappedCode()) break; + int? tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + { + Tag hereMention = new(TagType.HereMention, index, "(met)here(met)".Length, 0, guild?.EveryoneRole); + tags.Insert(tagIndex.Value, hereMention); + } + index++; + } + } - if (CheckWrappedCode()) break; + return tags.ToImmutable(); - int? tagIndex = FindIndex(tags, index); - if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, - new Tag(TagType.EveryoneMention, index, "(met)all(met)".Length, 0, guild?.EveryoneRole)); + // checks if the tag being parsed is wrapped in code blocks + bool CheckWrappedCode() + { + // PlainText mode + if (tagMode == TagMode.PlainText) + return false; - index++; - } + // loop through all code blocks that are before the start of the tag + while (codeIndex < index) + { + Match blockMatch = BlockCodeRegex.Match(text, codeIndex); + if (blockMatch.Success) + { + if (EnclosedInBlock(blockMatch)) + return true; - index = 0; - codeIndex = 0; - while (true) - { - index = text.IndexOf("@在线成员", index, StringComparison.Ordinal); - if (index == -1) break; + // continue if the end of the current code was before the start of the tag + codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length; + if (codeIndex < index) + continue; - if (CheckWrappedCode()) break; + return false; + } - int? tagIndex = FindIndex(tags, index); - if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, - new Tag(TagType.HereMention, index, "@在线成员".Length, 0, null)); + Match inlineMatch = InlineCodeRegex.Match(text, codeIndex); + if (inlineMatch.Success) + { + if (EnclosedInBlock(inlineMatch)) + return true; - index++; - } + // continue if the end of the current code was before the start of the tag + codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length; + if (codeIndex < index) + continue; - index = 0; - codeIndex = 0; - while (true) - { - index = text.IndexOf("(met)here(met)", index, StringComparison.Ordinal); - if (index == -1) break; + return false; + } - if (CheckWrappedCode()) break; + return false; + } - int? tagIndex = FindIndex(tags, index); - if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, - new Tag(TagType.HereMention, index, "(met)here(met)".Length, 0, guild?.EveryoneRole)); + return false; - index++; + // util to check if the index of a tag is within the bounds of the codeblock + bool EnclosedInBlock(Match m) => m.Groups[1].Index < index && index < m.Groups[2].Index; } - - return tags.ToImmutable(); } private static int? FindIndex(IReadOnlyList tags, int index) @@ -455,56 +510,55 @@ bool EnclosedInBlock(Match m) for (; i < tags.Count; i++) { ITag tag = tags[i]; - if (index < tag.Index) break; //Position before this tag + if (index < tag.Index) + break; //Position before this tag } - if (i > 0 && index < tags[i - 1].Index + tags[i - 1].Length) return null; //Overlaps tag before this + if (i > 0 && index < tags[i - 1].Index + tags[i - 1].Length) + return null; //Overlaps tag before this return i; } public static MessageSource GetSource(Message msg) { - if (msg.Author.Bot == true) return MessageSource.Bot; - if (msg.Author.IsSystemUser ?? msg.Author.Id == KookConfig.SystemMessageAuthorID) return MessageSource.System; - + if (msg.Author.Bot is true) + return MessageSource.Bot; return MessageSource.User; } - public static MessageSource GetSource(DirectMessage msg, IUser author) + public static MessageSource GetSource(IUser author) { - if (author.IsBot == true) return MessageSource.Bot; - - if (author.IsSystemUser ?? msg.AuthorId == KookConfig.SystemMessageAuthorID) - return MessageSource.System; - - return MessageSource.User; + return author switch + { + { IsSystemUser: true } => MessageSource.System, + { IsBot: true } => MessageSource.Bot, + _ => MessageSource.User + }; } - public static IUser GetAuthor(BaseKookClient client, IGuild guild, UserModel model) + public static async Task GetAuthorAsync(BaseKookClient client, IGuild? guild, UserModel model) { - IUser author = null; - if (guild != null) - // TODO: Migrate to async call - author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - - if (author == null) - author = RestUser.Create(client, model); - - return author; + IUser? author = guild is not null + ? await guild.GetUserAsync(model.Id, CacheMode.CacheOnly) + : null; + return author ?? RestUser.Create(client, model); } - public static async Task GetAuthorAsync(BaseKookClient client, IGuild guild, UserModel model) + public static IUser GetAuthor(BaseKookClient client, IGuild? guild, UserModel model) { - IUser author = null; - if (guild != null) - author = await guild.GetUserAsync(model.Id, CacheMode.CacheOnly); - - if (author == null) - author = RestUser.Create(client, model); + IUser? author = guild?.GetUserAsync(model.Id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + return author ?? RestUser.Create(client, model); + } - return author; + public static IUser GetAuthor(BaseKookClient client, IDMChannel channel, DirectMessage model) + { + if (client.CurrentUser is null) + throw new InvalidOperationException("The current user is not set well via login."); + return model.AuthorId == channel.Recipient.Id + ? channel.Recipient + : client.CurrentUser; } } diff --git a/src/Kook.Net.Rest/Entities/Messages/Poke.cs b/src/Kook.Net.Rest/Entities/Messages/Poke.cs index 69c7fc4d..eb1760ab 100644 --- a/src/Kook.Net.Rest/Entities/Messages/Poke.cs +++ b/src/Kook.Net.Rest/Entities/Messages/Poke.cs @@ -6,7 +6,7 @@ namespace Kook.Rest; /// /// Represents a poke in messages. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class Poke : IPoke { /// @@ -60,8 +60,12 @@ internal static Poke Create(Model model) TimeSpan cooldown = TimeSpan.FromSeconds(model.Cooldown); PokeLabel label = PokeLabel.Create(model.LabelId, model.LabelName); PokeIcon icon = PokeIcon.Create(model.Icon, model.IconExpired); - PokeQuality quality = PokeQuality.Create(model.QualityId, model.Quality.Color, - new Dictionary() { ["small"] = model.Quality.Small, ["big"] = model.Quality.Big }); + Dictionary resources = new() + { + ["small"] = model.Quality.Small, + ["big"] = model.Quality.Big + }; + PokeQuality quality = PokeQuality.Create(model.QualityId, model.Quality.Color, resources); IPokeResource pokeResource = model.Resource.ToEntity(); return new Poke(model.Id, model.Name, model.Description, cooldown, model.Categories, label, icon, quality, pokeResource, model.MessageScenarios); diff --git a/src/Kook.Net.Rest/Entities/Messages/RestMessage.cs b/src/Kook.Net.Rest/Entities/Messages/RestMessage.cs index de1d6193..20120f01 100644 --- a/src/Kook.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Kook.Net.Rest/Entities/Messages/RestMessage.cs @@ -1,5 +1,6 @@ using Kook.API; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Kook.Rest; @@ -8,8 +9,8 @@ namespace Kook.Rest; /// public abstract class RestMessage : RestEntity, IMessage, IUpdateable { - private ImmutableArray _reactions = ImmutableArray.Create(); - private ImmutableArray _userMentions = ImmutableArray.Create(); + private ImmutableArray _reactions = []; + private ImmutableArray _userMentions = []; /// public MessageType Type { get; } @@ -41,10 +42,10 @@ public abstract class RestMessage : RestEntity, IMessage, IUpdateable public DateTimeOffset? EditedTimestamp { get; private set; } /// - public virtual bool? MentionedEveryone => false; + public virtual bool MentionedEveryone => false; /// - public virtual bool? MentionedHere => false; + public virtual bool MentionedHere => false; /// /// Gets a collection of the 's on the message. @@ -73,7 +74,7 @@ public abstract class RestMessage : RestEntity, IMessage, IUpdateable public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); /// - public virtual bool? IsPinned => null; + public virtual bool? IsPinned { get; internal set; } /// /// Gets the of the message. @@ -91,22 +92,22 @@ internal RestMessage(BaseKookClient kook, Guid id, MessageType messageType, Channel = channel; Author = author; Source = source; + Content = string.Empty; + Attachments = []; } internal static RestMessage Create(BaseKookClient kook, IMessageChannel channel, IUser author, Message model) { - if (model.Author.IsSystemUser ?? model.Author.Id == KookConfig.SystemMessageAuthorID) - return RestSystemMessage.Create(kook, channel, author, model); - else - return RestUserMessage.Create(kook, channel, author, model); + return MessageHelper.GetSource(model) is MessageSource.System + ? RestSystemMessage.Create(kook, channel, author, model) + : RestUserMessage.Create(kook, channel, author, model); } internal static RestMessage Create(BaseKookClient kook, IMessageChannel channel, IUser author, DirectMessage model) { - if (author.IsSystemUser ?? model.AuthorId == KookConfig.SystemMessageAuthorID) - return RestSystemMessage.Create(kook, channel, author, model); - else - return RestUserMessage.Create(kook, channel, author, model); + return MessageHelper.GetSource(author) is MessageSource.System + ? RestSystemMessage.Create(kook, channel, author, model) + : RestUserMessage.Create(kook, channel, author, model); } internal virtual void Update(Message model) @@ -116,36 +117,10 @@ internal virtual void Update(Message model) Content = model.Content; if (model.Reactions is not null) - { - Reaction[] value = model.Reactions; - if (value.Length > 0) - { - ImmutableArray.Builder reactions = ImmutableArray.CreateBuilder(value.Length); - foreach (Reaction reaction in value) reactions.Add(RestReaction.Create(reaction)); - - _reactions = reactions.ToImmutable(); - } - else - _reactions = ImmutableArray.Create(); - } - else - _reactions = ImmutableArray.Create(); + _reactions = [..model.Reactions.Select(RestReaction.Create)]; if (model.MentionInfo?.MentionedUsers is not null) - { - MentionedUser[] value = model.MentionInfo.MentionedUsers; - if (value.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - MentionedUser val = value[i]; - if (val != null) newMentions.Add(RestUser.Create(Kook, val)); - } - - _userMentions = newMentions.ToImmutable(); - } - } + _userMentions = [..model.MentionInfo.MentionedUsers.Select(x => RestUser.Create(Kook, x))]; } internal virtual void Update(DirectMessage model) @@ -155,53 +130,30 @@ internal virtual void Update(DirectMessage model) Content = model.Content; if (model.Reactions is not null) - { - Reaction[] value = model.Reactions; - if (value.Length > 0) - { - ImmutableArray.Builder reactions = ImmutableArray.CreateBuilder(value.Length); - foreach (Reaction reaction in value) reactions.Add(RestReaction.Create(reaction)); - - _reactions = reactions.ToImmutable(); - } - else - _reactions = ImmutableArray.Create(); - } - else - _reactions = ImmutableArray.Create(); + _reactions = [..model.Reactions.Select(RestReaction.Create)]; if (model.MentionInfo?.MentionedUsers is not null) - { - MentionedUser[] value = model.MentionInfo.MentionedUsers; - if (value.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - MentionedUser val = value[i]; - if (val != null) newMentions.Add(RestUser.Create(Kook, val)); - } - - _userMentions = newMentions.ToImmutable(); - } - } + _userMentions = [..model.MentionInfo.MentionedUsers.Select(x => RestUser.Create(Kook, x))]; } /// - public IReadOnlyDictionary Reactions - => _reactions.ToDictionary( + public IReadOnlyDictionary Reactions => + _reactions.ToDictionary( x => x.Emote, - x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); + x => new ReactionMetadata + { + ReactionCount = x.Count, + IsMe = x.Me + }); /// - public Task DeleteAsync(RequestOptions options = null) - => MessageHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => MessageHelper.DeleteAsync(this, Kook, options); /// /// /// This message is neither a guild channel message nor a direct message. /// - public async Task UpdateAsync(RequestOptions options = null) + public async Task UpdateAsync(RequestOptions? options = null) { if (Channel is IGuildChannel) { @@ -212,49 +164,50 @@ public async Task UpdateAsync(RequestOptions options = null) if (Channel is IDMChannel dmChannel) { - DirectMessage model = await Kook.ApiClient.GetDirectMessageAsync(Id, dmChannel.ChatCode, options) + DirectMessage model = await Kook.ApiClient + .GetDirectMessageAsync(Id, dmChannel.ChatCode, options) .ConfigureAwait(false); Update(model); return; } - throw new InvalidOperationException("Unable to update a message that is neither a guild channel message nor a direct message."); + throw new NotSupportedException("The operation is not supported for this message type."); } /// - public Task AddReactionAsync(IEmote emote, RequestOptions options = null) => + public Task AddReactionAsync(IEmote emote, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.AddReactionAsync(this, emote, Kook, options), IDMChannel => MessageHelper.AddDirectMessageReactionAsync(this, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) => + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Kook, options), IDMChannel => MessageHelper.RemoveDirectMessageReactionAsync(this, user.Id, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) => + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.RemoveReactionAsync(this, userId, emote, Kook, options), IDMChannel => MessageHelper.RemoveDirectMessageReactionAsync(this, userId, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task> GetReactionUsersAsync(IEmote emote, RequestOptions options = null) => + public Task> GetReactionUsersAsync(IEmote emote, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.GetReactionUsersAsync(this, emote, Kook, options), IDMChannel => MessageHelper.GetDirectMessageReactionUsersAsync(this, emote, Kook, options), - _ => Task.FromResult>(null) + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; #region IMessage diff --git a/src/Kook.Net.Rest/Entities/Messages/RestReaction.cs b/src/Kook.Net.Rest/Entities/Messages/RestReaction.cs index 82c7bb47..95c77c3a 100644 --- a/src/Kook.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Kook.Net.Rest/Entities/Messages/RestReaction.cs @@ -29,12 +29,9 @@ internal RestReaction(IEmote emote, int count, bool me) internal static RestReaction Create(Model model) { - IEmote emote; - if (Emoji.TryParse(model.Emoji.Id, out Emoji emoji)) - emote = emoji; - else - emote = new Emote(model.Emoji.Id, model.Emoji.Name); - + IEmote emote = Emoji.TryParse(model.Emoji.Id, out Emoji? emoji) + ? emoji + : new Emote(model.Emoji.Id, model.Emoji.Name); return new RestReaction(emote, model.Count, model.IsMe); } } diff --git a/src/Kook.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Kook.Net.Rest/Entities/Messages/RestSystemMessage.cs index e2abb10a..598ccb5b 100644 --- a/src/Kook.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Kook.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -6,11 +6,11 @@ namespace Kook.Rest; /// /// Represents a REST-based system message. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestSystemMessage : RestMessage, ISystemMessage { /// - public SystemMessageType SystemMessageType { get; } + public SystemMessageType SystemMessageType { get; private set; } internal RestSystemMessage(BaseKookClient kook, Guid id, MessageType messageType, IMessageChannel channel, IUser author) : base(kook, id, messageType, channel, author, MessageSource.System) diff --git a/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs index 9bd6f225..91da9554 100644 --- a/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -7,26 +7,25 @@ namespace Kook.Rest; /// /// Represents a REST-based message sent by a user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestUserMessage : RestMessage, IUserMessage { - private bool? _isMentioningEveryone; - private bool? _isMentioningHere; - private Quote _quote; - private ImmutableArray _attachments = ImmutableArray.Create(); - private ImmutableArray _cards = ImmutableArray.Create(); - private ImmutableArray _embeds; - private ImmutableArray _pokes; - private ImmutableArray _roleMentionIds = ImmutableArray.Create(); - private ImmutableArray _roleMentions = ImmutableArray.Create(); - private ImmutableArray _channelMentions = ImmutableArray.Create(); - private ImmutableArray _tags = ImmutableArray.Create(); - - /// - public Quote Quote => _quote; - - /// - public new bool? IsPinned { get; internal set; } + private bool _isMentioningEveryone; + private bool _isMentioningHere; + private ImmutableArray _attachments = []; + private ImmutableArray _cards = []; + private ImmutableArray _embeds = []; + private ImmutableArray _pokes = []; + private ImmutableArray _roleMentionIds = []; + private ImmutableArray _roleMentions = []; + private ImmutableArray _channelMentions = []; + private ImmutableArray _tags = []; + + /// + public IQuote? Quote { get; private set; } + + /// + public override bool? IsPinned { get; internal set; } /// public override IReadOnlyCollection Attachments => _attachments; @@ -41,10 +40,10 @@ public class RestUserMessage : RestMessage, IUserMessage public override IReadOnlyCollection Pokes => _pokes; /// - public override bool? MentionedEveryone => _isMentioningEveryone; + public override bool MentionedEveryone => _isMentioningEveryone; /// - public override bool? MentionedHere => _isMentioningHere; + public override bool MentionedHere => _isMentioningHere; /// public override IReadOnlyCollection MentionedRoleIds => _roleMentionIds; @@ -62,21 +61,26 @@ public class RestUserMessage : RestMessage, IUserMessage /// public override IReadOnlyCollection Tags => _tags; - internal RestUserMessage(BaseKookClient kook, Guid id, MessageType messageType, IMessageChannel channel, IUser author, MessageSource source) + internal RestUserMessage(BaseKookClient kook, Guid id, MessageType messageType, + IMessageChannel channel, IUser author, MessageSource source) : base(kook, id, messageType, channel, author, source) { } - internal static new RestUserMessage Create(BaseKookClient kook, IMessageChannel channel, IUser author, Message model) + internal static new RestUserMessage Create(BaseKookClient kook, + IMessageChannel channel, IUser author, Message model) { - RestUserMessage entity = new(kook, model.Id, model.Type, channel, author, MessageHelper.GetSource(model)); + MessageSource messageSource = MessageHelper.GetSource(model); + RestUserMessage entity = new(kook, model.Id, model.Type, channel, author, messageSource); entity.Update(model); return entity; } - internal static new RestUserMessage Create(BaseKookClient kook, IMessageChannel channel, IUser author, DirectMessage model) + internal static new RestUserMessage Create(BaseKookClient kook, + IMessageChannel channel, IUser author, DirectMessage model) { - RestUserMessage entity = new(kook, model.Id, model.Type, channel, author, MessageHelper.GetSource(model, author)); + MessageSource messageSource = MessageHelper.GetSource(author); + RestUserMessage entity = new(kook, model.Id, model.Type, channel, author, messageSource); entity.Update(model); return entity; } @@ -84,114 +88,84 @@ internal RestUserMessage(BaseKookClient kook, Guid id, MessageType messageType, internal override void Update(Message model) { base.Update(model); - ulong? guildId = (Channel as IGuildChannel)?.GuildId; - IGuild guild = guildId != null ? (Kook as IKookClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).GetAwaiter().GetResult() : null; - if (model.Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, null, guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, null, guild, MentionedUsers, TagMode.KMarkdown); + IGuild? guild = (Channel as IGuildChannel)?.Guild; + _tags = model.Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; _isMentioningEveryone = model.MentionedAll; _isMentioningHere = model.MentionedHere; - _roleMentionIds = model.MentionedRoles.ToImmutableArray(); + _roleMentionIds = [..model.MentionedRoles]; if (Channel is IGuildChannel guildChannel) { - if (model.MentionInfo?.MentionedRoles is not null) - { - Role[] value = model.MentionInfo.MentionedRoles; - if (value.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - Role val = value[i]; - if (val != null) newMentions.Add(RestRole.Create(Kook, guildChannel.Guild, val)); - } - - _roleMentions = newMentions.ToImmutable(); - } - } - - if (model.MentionInfo?.MentionedChannels is not null) - { - MentionedChannel[] value = model.MentionInfo.MentionedChannels; - if (value.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - MentionedChannel val = value[i]; - if (val != null) newMentions.Add(RestGuildChannel.Create(Kook, guildChannel.Guild, val)); - } - - _channelMentions = newMentions.ToImmutable(); - } - } + if (model.MentionInfo?.MentionedRoles is { } roles) + _roleMentions = [..roles.Select(x => RestRole.Create(Kook, guildChannel.Guild, x))]; + if (model.MentionInfo?.MentionedChannels is { } channels) + _channelMentions = [..channels.Select(x => RestGuildChannel.Create(Kook, guildChannel.Guild, x))]; } - if (model.Quote is not null) + if (model.Quote is { } quote) { - IUser refMsgAuthor = MessageHelper.GetAuthor(Kook, null, model.Quote.Author); - _quote = Quote.Create(model.Quote.Id, model.Quote.QuotedMessageId, model.Quote.Type, model.Quote.Content, model.Quote.CreateAt, - refMsgAuthor); + IUser refMsgAuthor = MessageHelper.GetAuthor(Kook, guild, model.Quote.Author); + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, refMsgAuthor); } - if (model.Attachment is not null) _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { _cards = MessageHelper.ParseCards(model.Content); - _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; } - _embeds = model.Embeds.Select(x => x.ToEntity()).ToImmutableArray(); + _embeds = [..model.Embeds.Select(x => x.ToEntity())]; - _pokes = Type == MessageType.Poke && model.MentionInfo?.Pokes is not null - ? model.MentionInfo.Pokes.Select(x => RestPokeAction.Create(Kook, Author, - model.MentionInfo.MentionedUsers.Select(y => RestUser.Create(Kook, y)), x)).ToImmutableArray() - : ImmutableArray.Empty; + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => RestPokeAction.Create(Kook, Author, MentionedUsers, x))]; } internal override void Update(DirectMessage model) { base.Update(model); - ulong? guildId = (Channel as IGuildChannel)?.GuildId; - IGuild guild = guildId != null ? (Kook as IKookClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).GetAwaiter().GetResult() : null; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, null, guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, null, guild, MentionedUsers, TagMode.KMarkdown); + _tags = model.Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, null, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, null, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; - if (model.Quote is not null) + if (model.Quote is { } quote) { IUser refMsgAuthor = MessageHelper.GetAuthor(Kook, null, model.Quote.Author); - _quote = Quote.Create(model.Quote.Id, model.Quote.QuotedMessageId, model.Quote.Type, model.Quote.Content, model.Quote.CreateAt, - refMsgAuthor); + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, refMsgAuthor); } - if (model.Attachment is not null) - _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { _cards = MessageHelper.ParseCards(model.Content); - _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; } - _embeds = model.Embeds.Select(x => x.ToEntity()).ToImmutableArray(); + _embeds = [..model.Embeds.Select(x => x.ToEntity())]; - if (Type == MessageType.Poke && model.MentionInfo?.Pokes is not null) - { - IUser recipient = (Channel as IDMChannel)?.Recipient; - IUser target = recipient is null - ? null - : recipient.Id == Author.Id - ? Kook.CurrentUser - : recipient; - _pokes = model.MentionInfo.Pokes.Select(x => RestPokeAction.Create(Kook, Author, - new[] { target }, x)).ToImmutableArray(); - } - else - _pokes = ImmutableArray.Empty; + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => RestPokeAction.Create(Kook, Author, MentionedUsers, x))]; } /// @@ -203,37 +177,43 @@ internal override void Update(DirectMessage model) /// Determines how the role tag should be handled. /// Determines how the @everyone tag should be handled. /// Determines how the emoji tag should be handled. - public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, - TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, + TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => + MentionUtils.Resolve(this, startIndex, + userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); /// - public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, - TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - - private string DebuggerDisplay => - $"{Author}: {Content} ({Id}{(Attachments is not null && Attachments.Any() ? $", {Attachments.Count} Attachment{(Attachments.Count == 1 ? string.Empty : "s")}" : string.Empty)})"; + public string Resolve(TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, + TagHandling everyoneHandling = TagHandling.Name, TagHandling emojiHandling = TagHandling.Name) => + MentionUtils.Resolve(this, 0, + userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{ + Attachments.Count switch + { + 0 => string.Empty, + 1 => ", 1 Attachment", + _ => $", {Attachments.Count} Attachments" + }})"; #region IUserMessage /// - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions? options = null) { await MessageHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); MessageProperties properties = new() { Content = Content, Cards = Cards, Quote = Quote }; func(properties); Content = properties.Content; _cards = properties.Cards?.ToImmutableArray() ?? ImmutableArray.Empty; - _quote = properties.Quote?.QuotedMessageId == Guid.Empty ? null : (Quote)properties.Quote; + Quote = properties.Quote?.QuotedMessageId == Guid.Empty ? null : properties.Quote; } /// bool? IMessage.IsPinned => IsPinned; - /// - IQuote IUserMessage.Quote => _quote; - /// IReadOnlyCollection IMessage.Cards => Cards; diff --git a/src/Kook.Net.Rest/Entities/Roles/RestRole.cs b/src/Kook.Net.Rest/Entities/Roles/RestRole.cs index b00bddea..1dc371db 100644 --- a/src/Kook.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Kook.Net.Rest/Entities/Roles/RestRole.cs @@ -6,7 +6,7 @@ namespace Kook.Rest; /// /// Represents a REST-based role. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestRole : RestEntity, IRole { #region RestRole @@ -20,7 +20,7 @@ public class RestRole : RestEntity, IRole internal IGuild Guild { get; } /// - public RoleType? Type { get; private set; } + public RoleType Type { get; private set; } /// public Color Color { get; private set; } @@ -61,8 +61,11 @@ public class RestRole : RestEntity, IRole public string PlainTextMention => IsEveryone ? "@全体成员" : MentionUtils.PlainTextMentionRole(Id); internal RestRole(BaseKookClient kook, IGuild guild, uint id) - : base(kook, id) => + : base(kook, id) + { Guild = guild; + Name = string.Empty; + } internal static RestRole Create(BaseKookClient kook, IGuild guild, Model model) { @@ -78,56 +81,46 @@ internal void Update(Model model) Color = model.Color; ColorType = model.ColorType; GradientColor = model.GradientColor; + Position = model.Position; IsHoisted = model.Hoist; IsMentionable = model.Mentionable; - Position = model.Position; Permissions = new GuildPermissions(model.Permissions); } /// - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions? options = null) { Model model = await RoleHelper.ModifyAsync(this, Kook, func, options).ConfigureAwait(false); Update(model); } /// - public Task DeleteAsync(RequestOptions options = null) - => RoleHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + RoleHelper.DeleteAsync(this, Kook, options); - /// - public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) - { - void Func(SearchGuildMemberProperties p) => p.RoleId = Id; - return GuildHelper.SearchUsersAsync(Guild, Kook, Func, KookConfig.MaxUsersPerBatch, options); - } + /// + /// Gets a collection of users that have this role. + /// + /// The options to be used when fetching the users. + /// An asynchronous enumerable that contains a collection of users that have this role. + public IAsyncEnumerable> GetUsersAsync(RequestOptions? options = null) => + GuildHelper.SearchUsersAsync(Guild, Kook, x => x.RoleId = Id, KookConfig.MaxUsersPerBatch, options); /// - public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + public int CompareTo(IRole? role) => RoleUtils.Compare(this, role); #endregion #region IRole /// - IGuild IRole.Guild - { - get - { - if (Guild != null) return Guild; - - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } + IGuild IRole.Guild => Guild; /// - IAsyncEnumerable> IRole.GetUsersAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IRole.GetUsersAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); #endregion diff --git a/src/Kook.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Kook.Net.Rest/Entities/Roles/RoleHelper.cs index 414d95bf..400c53a8 100644 --- a/src/Kook.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Kook.Net.Rest/Entities/Roles/RoleHelper.cs @@ -7,19 +7,21 @@ internal static class RoleHelper { #region General - public static async Task DeleteAsync(IRole role, BaseKookClient client, - RequestOptions options) + public static async Task DeleteAsync(IRole role, BaseKookClient client, RequestOptions? options) { - DeleteGuildRoleParams args = new() { Id = role.Id, GuildId = role.Guild.Id }; + DeleteGuildRoleParams args = new() + { + Id = role.Id, + GuildId = role.Guild.Id + }; await client.ApiClient.DeleteGuildRoleAsync(args, options).ConfigureAwait(false); } public static async Task ModifyAsync(IRole role, BaseKookClient client, - Action func, RequestOptions options) + Action func, RequestOptions? options) { RoleProperties args = new(); func(args); - ModifyGuildRoleParams apiArgs = new() { GuildId = role.Guild.Id, @@ -30,13 +32,13 @@ public static async Task ModifyAsync(IRole role, BaseKookClient client, { true => 1, false => 0, - null => null + _ => null }, Mentionable = args.Mentionable switch { true => 1, false => 0, - null => null + _ => null }, Permissions = args.Permissions?.RawValue }; diff --git a/src/Kook.Net.Rest/Entities/Users/RestFriendRequest.cs b/src/Kook.Net.Rest/Entities/Users/RestFriendRequest.cs index 7e2d0724..9a9bbffb 100644 --- a/src/Kook.Net.Rest/Entities/Users/RestFriendRequest.cs +++ b/src/Kook.Net.Rest/Entities/Users/RestFriendRequest.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Model = Kook.API.Rest.FriendState; namespace Kook.Rest; @@ -6,34 +7,36 @@ namespace Kook.Rest; /// /// Represents a REST-based friend request. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestFriendRequest : RestEntity, IFriendRequest { /// - internal RestFriendRequest(BaseKookClient kook, ulong id) : base(kook, id) + public IUser User { get; internal set; } + + /// + internal RestFriendRequest(BaseKookClient kook, Model model) : + base(kook, model.Id) { + Update(model); } - internal static RestFriendRequest Create(BaseKookClient kook, Model model) + internal static RestFriendRequest Create(BaseKookClient kook, Model model) => new(kook, model); + + [MemberNotNull(nameof(User))] + internal void Update(Model model) { - RestFriendRequest entity = new(kook, model.Id); - entity.Update(model); - return entity; + User = RestUser.Create(Kook, model.User); } - internal virtual void Update(Model model) => User = RestUser.Create(Kook, model.User); - private string DebuggerDisplay => - $"{Format.UsernameAndIdentifyNumber(User, Kook.FormatUsersInBidirectionalUnicode)} ({Id}{(User.IsBot ?? false ? ", Bot" : "")})"; - - /// - public IUser User { get; internal set; } + $"{User.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (User.IsBot ?? false ? ", Bot" : "")}{(User.IsSystemUser ? ", System" : "")})"; /// - public Task AcceptAsync(RequestOptions options = null) => + public Task AcceptAsync(RequestOptions? options = null) => UserHelper.HandleFriendRequestAsync(this, true, Kook, options); /// - public Task DeclineAsync(RequestOptions options = null) => + public Task DeclineAsync(RequestOptions? options = null) => UserHelper.HandleFriendRequestAsync(this, false, Kook, options); } diff --git a/src/Kook.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Kook.Net.Rest/Entities/Users/RestGuildUser.cs index 770560bb..d6b01298 100644 --- a/src/Kook.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Kook.Net.Rest/Entities/Users/RestGuildUser.cs @@ -8,7 +8,7 @@ namespace Kook.Rest; /// /// Represents a REST-based guild user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestGuildUser : RestUser, IGuildUser { #region RestGuildUser @@ -19,7 +19,7 @@ public class RestGuildUser : RestUser, IGuildUser public string DisplayName => Nickname ?? Username; /// - public string Nickname { get; private set; } + public string? Nickname { get; private set; } internal IGuild Guild { get; private set; } @@ -27,16 +27,16 @@ public class RestGuildUser : RestUser, IGuildUser public ulong GuildId => Guild.Id; /// - public bool IsMobileVerified { get; private set; } + public bool? IsMobileVerified { get; private set; } /// - public DateTimeOffset JoinedAt { get; private set; } + public DateTimeOffset? JoinedAt { get; private set; } /// - public DateTimeOffset ActiveAt { get; private set; } + public DateTimeOffset? ActiveAt { get; private set; } /// - public Color Color { get; private set; } + public Color? Color { get; private set; } /// public bool? IsOwner { get; private set; } @@ -47,8 +47,8 @@ public GuildPermissions GuildPermissions { get { - if (!Guild.Available) throw new InvalidOperationException("Resolving permissions requires the parent guild to be downloaded."); - + if (!Guild.Available) + throw new InvalidOperationException("Resolving permissions requires the parent guild to be downloaded."); return new GuildPermissions(Permissions.ResolveGuild(Guild, this)); } } @@ -60,14 +60,16 @@ public GuildPermissions GuildPermissions public new string PlainTextMention => MentionUtils.PlainTextMentionUser(Nickname ?? Username, Id); internal RestGuildUser(BaseKookClient kook, IGuild guild, ulong id) - : base(kook, id) => + : base(kook, id) + { Guild = guild; + _roleIds = [0]; + } internal static RestGuildUser Create(BaseKookClient kook, IGuild guild, UserModel model) { RestGuildUser entity = new(kook, guild, model.Id); entity.Update(model); - entity.UpdateRoles(Array.Empty()); return entity; } @@ -78,6 +80,13 @@ internal static RestGuildUser Create(BaseKookClient kook, IGuild guild, MemberMo return entity; } + internal static RestGuildUser Create(BaseKookClient kook, IGuild guild, API.MentionedUser model) + { + RestGuildUser entity = new(kook, guild, model.Id); + entity.Update(model); + return entity; + } + internal void Update(MemberModel model) { base.Update(model); @@ -89,126 +98,124 @@ internal void Update(MemberModel model) ActiveAt = model.ActiveAt; Color = model.Color; IsOwner = model.IsOwner; - UpdateRoles(model.Roles); + if (model.Roles != null) + _roleIds = [0, ..model.Roles]; } - private void UpdateRoles(uint[] roleIds) + internal override void Update(API.MentionedUser model) { - ImmutableArray.Builder roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(0); - for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); - - _roleIds = roles.ToImmutable(); + base.Update(model); + if (DisplayName != Username) + Nickname = model.DisplayName; } /// - public override async Task UpdateAsync(RequestOptions options = null) + public override async Task UpdateAsync(RequestOptions? options = null) { MemberModel model = await Kook.ApiClient.GetGuildMemberAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } /// - public async Task ModifyNicknameAsync(string name, RequestOptions options = null) + public async Task ModifyNicknameAsync(string? name, RequestOptions? options = null) { - string nickname = await UserHelper.ModifyNicknameAsync(this, Kook, name, options); + string? nickname = await UserHelper.ModifyNicknameAsync(this, Kook, name, options); // The KOOK API will clear the nickname if the nickname is set to the same as the username at present. - if (nickname == Username) nickname = null; - - Nickname = nickname; + Nickname = nickname == Username ? null : nickname; } /// - public Task> GetBoostSubscriptionsAsync(RequestOptions options = null) - => UserHelper.GetBoostSubscriptionsAsync(this, Kook, options); + public Task> GetBoostSubscriptionsAsync( + RequestOptions? options = null) => + UserHelper.GetBoostSubscriptionsAsync(this, Kook, options); /// - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Kook, options); + public Task KickAsync(RequestOptions? options = null) => + UserHelper.KickAsync(this, Kook, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task AddRoleAsync(uint roleId, RequestOptions options = null) - => AddRolesAsync(new[] { roleId }, options); + public Task AddRoleAsync(uint roleId, RequestOptions? options = null) => + AddRolesAsync(new[] { roleId }, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRoleAsync(role.Id, options); + public Task AddRoleAsync(IRole role, RequestOptions? options = null) => + AddRoleAsync(role.Id, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Kook, roleIds, options); + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions? options = null) => + UserHelper.AddRolesAsync(this, Kook, roleIds, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => AddRolesAsync(roles.Select(x => x.Id), options); + public Task AddRolesAsync(IEnumerable roles, RequestOptions? options = null) => + AddRolesAsync(roles.Select(x => x.Id), options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task RemoveRoleAsync(uint roleId, RequestOptions options = null) - => RemoveRolesAsync(new[] { roleId }, options); + public Task RemoveRoleAsync(uint roleId, RequestOptions? options = null) => + RemoveRolesAsync(new[] { roleId }, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRoleAsync(role.Id, options); + public Task RemoveRoleAsync(IRole role, RequestOptions? options = null) => + RemoveRoleAsync(role.Id, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Kook, roleIds, options); + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions? options = null) => + UserHelper.RemoveRolesAsync(this, Kook, roleIds, options); /// /// /// This method will update the cached roles of this user. /// To update the cached roles of this user, please use . /// - public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => RemoveRolesAsync(roles.Select(x => x.Id)); + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions? options = null) => + RemoveRolesAsync(roles.Select(x => x.Id)); /// - public Task MuteAsync(RequestOptions options = null) - => GuildHelper.MuteUserAsync(this, Kook, options); + public Task MuteAsync(RequestOptions? options = null) => + GuildHelper.MuteUserAsync(this, Kook, options); /// - public Task DeafenAsync(RequestOptions options = null) - => GuildHelper.DeafenUserAsync(this, Kook, options); + public Task DeafenAsync(RequestOptions? options = null) => + GuildHelper.DeafenUserAsync(this, Kook, options); /// - public Task UnmuteAsync(RequestOptions options = null) - => GuildHelper.UnmuteUserAsync(this, Kook, options); + public Task UnmuteAsync(RequestOptions? options = null) => + GuildHelper.UnmuteUserAsync(this, Kook, options); /// - public Task UndeafenAsync(RequestOptions options = null) - => GuildHelper.UndeafenUserAsync(this, Kook, options); + public Task UndeafenAsync(RequestOptions? options = null) => + GuildHelper.UndeafenUserAsync(this, Kook, options); /// - public Task> GetConnectedVoiceChannelsAsync(RequestOptions options = null) - => UserHelper.GetConnectedChannelAsync(this, Kook, options); + public Task> GetConnectedVoiceChannelsAsync(RequestOptions? options = null) => + UserHelper.GetConnectedChannelAsync(this, Kook, options); /// /// Resolving permissions requires the parent guild to be downloaded. @@ -219,7 +226,7 @@ public ChannelPermissions GetPermissions(IGuildChannel channel) } /// - public override Task RequestFriendAsync(RequestOptions options = null) => + public override Task RequestFriendAsync(RequestOptions? options = null) => UserHelper.RequestFriendAsync(this, Kook, options); #endregion @@ -227,15 +234,7 @@ public override Task RequestFriendAsync(RequestOptions options = null) => #region IGuildUser /// - IGuild IGuildUser.Guild - { - get - { - if (Guild != null) return Guild; - - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } + IGuild IGuildUser.Guild => Guild; #endregion @@ -248,7 +247,7 @@ IGuild IGuildUser.Guild bool? IVoiceState.IsDeafened => null; /// - IVoiceChannel IVoiceState.VoiceChannel => null; + IVoiceChannel? IVoiceState.VoiceChannel => null; #endregion } diff --git a/src/Kook.Net.Rest/Entities/Users/RestPresence.cs b/src/Kook.Net.Rest/Entities/Users/RestPresence.cs index 67f0144f..190b09da 100644 --- a/src/Kook.Net.Rest/Entities/Users/RestPresence.cs +++ b/src/Kook.Net.Rest/Entities/Users/RestPresence.cs @@ -5,7 +5,7 @@ namespace Kook.Rest; /// /// Represents the REST user's presence status. This may include their online status and their activity. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestPresence : IPresence { /// @@ -31,10 +31,10 @@ internal static RestPresence Create(bool? isOnline, string activeClient) return entity; } - internal void Update(bool? isOnline, string activeClient) + internal void Update(bool? isOnline, string? activeClient) { - if (isOnline.HasValue) IsOnline = isOnline; - + if (isOnline.HasValue) + IsOnline = isOnline; ActiveClient = ConvertClientType(activeClient); } @@ -47,16 +47,21 @@ internal void Update(bool? isOnline, string activeClient) /// /// A that this user is active. /// - private static ClientType? ConvertClientType(string clientType) + private static ClientType? ConvertClientType(string? clientType) { - if (string.IsNullOrWhiteSpace(clientType)) return null; - - if (Enum.TryParse(clientType, true, out ClientType type)) return type; - + if (string.IsNullOrWhiteSpace(clientType)) + return null; + if (Enum.TryParse(clientType, true, out ClientType type)) + return type; return null; } - private string DebuggerDisplay => $"{IsOnline switch { true => "Online", false => "Offline", _ => "Unknown Status" }}, {ActiveClient.ToString()}"; + private string DebuggerDisplay => $"{IsOnline switch + { + true => "Online", + false => "Offline", + _ => "Unknown Status" + }}, {ActiveClient.ToString()}"; - internal RestPresence Clone() => MemberwiseClone() as RestPresence; + internal RestPresence Clone() => (RestPresence)MemberwiseClone(); } diff --git a/src/Kook.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Kook.Net.Rest/Entities/Users/RestSelfUser.cs index 3b0b6970..0aabb531 100644 --- a/src/Kook.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Kook.Net.Rest/Entities/Users/RestSelfUser.cs @@ -6,14 +6,14 @@ namespace Kook.Rest; /// /// Represents the logged-in REST-based user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestSelfUser : RestUser, ISelfUser { /// - public string MobilePrefix { get; private set; } + public string? MobilePrefix { get; private set; } /// - public string Mobile { get; private set; } + public string? Mobile { get; private set; } /// public int InvitedCount { get; private set; } @@ -24,6 +24,8 @@ public class RestSelfUser : RestUser, ISelfUser internal RestSelfUser(BaseKookClient kook, ulong id) : base(kook, id) { + MobilePrefix = string.Empty; + Mobile = string.Empty; } internal static RestSelfUser Create(BaseKookClient kook, Model model) @@ -36,35 +38,38 @@ internal static RestSelfUser Create(BaseKookClient kook, Model model) internal void Update(Model model) { base.Update(model); - MobilePrefix = model.MobilePrefix; Mobile = model.Mobile; - InvitedCount = model.InvitedCount ?? 0; + InvitedCount = model.InvitedCount; IsMobileVerified = model.MobileVerified; } /// /// Unable to update this object using a different token. - public override async Task UpdateAsync(RequestOptions options = null) + public override async Task UpdateAsync(RequestOptions? options = null) { Model model = await Kook.ApiClient.GetSelfUserAsync(options).ConfigureAwait(false); - if (model.Id != Id) throw new InvalidOperationException("Unable to update this object using a different token."); - + if (model.Id != Id) + throw new InvalidOperationException("Unable to update this object using a different token."); Update(model); } + private string DebuggerDisplay => + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")}, Self)"; + #region ISelfUser /// - public async Task StartPlayingAsync(IGame game, RequestOptions options = null) => + public async Task StartPlayingAsync(IGame game, RequestOptions? options = null) => await UserHelper.StartPlayingAsync(this, Kook, game, options).ConfigureAwait(false); /// - public async Task StartPlayingAsync(Music music, RequestOptions options = null) => + public async Task StartPlayingAsync(Music music, RequestOptions? options = null) => await UserHelper.StartPlayingAsync(this, Kook, music, options).ConfigureAwait(false); /// - public async Task StopPlayingAsync(ActivityType type, RequestOptions options = null) => + public async Task StopPlayingAsync(ActivityType type, RequestOptions? options = null) => await UserHelper.StopPlayingAsync(this, Kook, type, options).ConfigureAwait(false); #endregion diff --git a/src/Kook.Net.Rest/Entities/Users/RestUser.cs b/src/Kook.Net.Rest/Entities/Users/RestUser.cs index a4e24844..20b48e6a 100644 --- a/src/Kook.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Kook.Net.Rest/Entities/Users/RestUser.cs @@ -8,16 +8,18 @@ namespace Kook.Rest; /// /// Represents a REST-based user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestUser : RestEntity, IUser, IUpdateable { + private const int BannedStatus = 10; + #region RestUser /// public string Username { get; internal set; } /// - public ushort? IdentifyNumberValue { get; internal set; } + public ushort IdentifyNumberValue { get; internal set; } /// public bool? IsBot { get; internal set; } @@ -38,22 +40,22 @@ public class RestUser : RestEntity, IUser, IUpdateable public string BuffAvatar { get; internal set; } /// - public string Banner { get; internal set; } + public string? Banner { get; internal set; } /// public bool? IsDenoiseEnabled { get; internal set; } /// - public UserTag UserTag { get; internal set; } + public UserTag? UserTag { get; internal set; } /// public IReadOnlyCollection Nameplates { get; internal set; } /// - public bool? IsSystemUser { get; internal set; } + public bool IsSystemUser { get; internal set; } /// - public string IdentifyNumber => IdentifyNumberValue?.ToString("D4"); + public string IdentifyNumber => IdentifyNumberValue.ToString("D4"); /// public string KMarkdownMention => MentionUtils.KMarkdownMentionUser(Id); @@ -64,7 +66,7 @@ public class RestUser : RestEntity, IUser, IUpdateable internal RestPresence Presence { get; set; } /// - public bool? IsOnline => Presence?.IsOnline; + public bool? IsOnline => Presence.IsOnline; /// public ClientType? ActiveClient => Presence?.ActiveClient; @@ -72,6 +74,13 @@ public class RestUser : RestEntity, IUser, IUpdateable internal RestUser(BaseKookClient kook, ulong id) : base(kook, id) { + Username = string.Empty; + Avatar = string.Empty; + BuffAvatar = string.Empty; + Banner = string.Empty; + Nameplates = []; + Presence = new RestPresence(); + IsSystemUser = Id == KookConfig.SystemMessageAuthorID; } internal static RestUser Create(BaseKookClient kook, Model model) @@ -82,9 +91,6 @@ internal static RestUser Create(BaseKookClient kook, Model model) } internal static RestUser Create(BaseKookClient kook, API.MentionedUser model) - => Create(kook, null, model); - - internal static RestUser Create(BaseKookClient kook, IGuild guild, API.MentionedUser model) { RestUser entity = new(kook, model.Id); entity.Update(model); @@ -94,9 +100,9 @@ internal static RestUser Create(BaseKookClient kook, IGuild guild, API.Mentioned internal virtual void Update(Model model) { Username = model.Username; - IdentifyNumberValue = ushort.Parse(model.IdentifyNumber, NumberStyles.None, CultureInfo.InvariantCulture); + IdentifyNumberValue = ushort.Parse(model.IdentifyNumber); IsBot = model.Bot; - IsBanned = model.Status == 10; + IsBanned = model.Status == BannedStatus; HasBuff = model.HasBuff; HasAnnualBuff = model.HasAnnualBuff; Avatar = model.Avatar; @@ -104,36 +110,35 @@ internal virtual void Update(Model model) BuffAvatar = model.BuffAvatar; IsDenoiseEnabled = model.IsDenoiseEnabled; UserTag = model.UserTag?.ToEntity(); - Nameplates = model.Nameplates?.Select(x => x.ToEntity()).ToImmutableArray(); - IsSystemUser = model.IsSystemUser; - + if (model.Nameplates is not null) + Nameplates = [..model.Nameplates.Select(x => x.ToEntity())]; + if (model.IsSystemUser.HasValue) + IsSystemUser = model.IsSystemUser.Value; UpdatePresence(model.Online, model.OperatingSystem); } internal virtual void Update(API.MentionedUser model) { - Username = model.Username; - IdentifyNumberValue = model.FullName.Length > 4 -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - && ushort.TryParse(model.FullName[^4..], out ushort val) -#else - && ushort.TryParse(model.FullName.Substring(model.FullName.Length - 4), out ushort val) -#endif - ? val - : null; + Username = model.DisplayName; + if (model.FullName.Length >= 5) + { + IdentifyNumberValue = ushort.Parse(model.FullName[^4..]); + Username = model.FullName[..^5]; + } + else + Username = model.DisplayName; Avatar = model.Avatar; } /// - public virtual async Task UpdateAsync(RequestOptions options = null) + public virtual async Task UpdateAsync(RequestOptions? options = null) { Model model = await Kook.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); Update(model); } - internal virtual void UpdatePresence(bool? isOnline, string activeClient) + internal virtual void UpdatePresence(bool? isOnline, string? activeClient) { - Presence ??= new RestPresence(); Presence.Update(isOnline, activeClient); } @@ -144,8 +149,8 @@ internal virtual void UpdatePresence(bool? isOnline, string activeClient) /// /// A task that represents the asynchronous get operation. The task result contains a rest DM channel where the user is the recipient. /// - public Task CreateDMChannelAsync(RequestOptions options = null) - => UserHelper.CreateDMChannelAsync(this, Kook, options); + public Task CreateDMChannelAsync(RequestOptions? options = null) => + UserHelper.CreateDMChannelAsync(this, Kook, options); /// /// Gets the intimacy information with this user. @@ -155,27 +160,27 @@ public Task CreateDMChannelAsync(RequestOptions options = null) /// A task that represents the asynchronous operation for getting the intimacy information. The task result /// contains the intimacy information associated with this user. /// - public Task GetIntimacyAsync(RequestOptions options = null) - => UserHelper.GetIntimacyAsync(this, Kook, options); + public Task GetIntimacyAsync(RequestOptions? options = null) => + UserHelper.GetIntimacyAsync(this, Kook, options); /// - public async Task UpdateIntimacyAsync(Action func, RequestOptions options = null) => + public async Task UpdateIntimacyAsync(Action func, RequestOptions? options = null) => await UserHelper.UpdateIntimacyAsync(this, Kook, func, options).ConfigureAwait(false); /// - public Task BlockAsync(RequestOptions options = null) => + public Task BlockAsync(RequestOptions? options = null) => UserHelper.BlockAsync(this, Kook, options); /// - public Task UnblockAsync(RequestOptions options = null) => + public Task UnblockAsync(RequestOptions? options = null) => UserHelper.UnblockAsync(this, Kook, options); /// - public virtual Task RequestFriendAsync(RequestOptions options = null) => + public virtual Task RequestFriendAsync(RequestOptions? options = null) => UserHelper.RequestFriendAsync(this, Kook, options); /// - public Task RemoveFriendAsync(RequestOptions options = null) => + public Task RemoveFriendAsync(RequestOptions? options = null) => UserHelper.RemoveFriendAsync(this, Kook, options); #endregion @@ -186,20 +191,21 @@ public Task RemoveFriendAsync(RequestOptions options = null) => /// /// A string that resolves to Username#IdentifyNumber of the user. /// - public override string ToString() => Format.UsernameAndIdentifyNumber(this, Kook.FormatUsersInBidirectionalUnicode); + public override string ToString() => this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode); private string DebuggerDisplay => - $"{Format.UsernameAndIdentifyNumber(this, Kook.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ?? false ? ", Bot" : "")})"; + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")})"; #region IUser /// - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.CreateDMChannelAsync(RequestOptions? options) => + await CreateDMChannelAsync(options).ConfigureAwait(false); /// - async Task IUser.GetIntimacyAsync(RequestOptions options) - => await GetIntimacyAsync(options).ConfigureAwait(false); + async Task IUser.GetIntimacyAsync(RequestOptions? options) => + await GetIntimacyAsync(options).ConfigureAwait(false); #endregion } diff --git a/src/Kook.Net.Rest/Entities/Users/UserHelper.cs b/src/Kook.Net.Rest/Entities/Users/UserHelper.cs index 03bc50b6..3717bdbd 100644 --- a/src/Kook.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Kook.Net.Rest/Entities/Users/UserHelper.cs @@ -6,133 +6,190 @@ namespace Kook.Rest; internal static class UserHelper { - public static async Task ModifyNicknameAsync(IGuildUser user, BaseKookClient client, - string nickname, RequestOptions options) + public static async Task ModifyNicknameAsync(IGuildUser user, BaseKookClient client, + string? nickname, RequestOptions? options) { ModifyGuildMemberNicknameParams args = new() { GuildId = user.GuildId, - Nickname = string.IsNullOrEmpty(nickname) ? null : nickname, - UserId = user.Id == client.CurrentUser.Id ? null : user.Id + Nickname = nickname, + UserId = user.Id == client.CurrentUser?.Id ? null : user.Id }; await client.ApiClient.ModifyGuildMemberNicknameAsync(args, options).ConfigureAwait(false); return nickname; } - public static async Task> GetBoostSubscriptionsAsync(IGuildUser guildUser, - BaseKookClient client, RequestOptions options) + public static async Task> GetBoostSubscriptionsAsync( + IGuildUser guildUser, BaseKookClient client, RequestOptions? options) { IEnumerable subscriptions = await client.ApiClient .GetGuildBoostSubscriptionsAsync(guildUser.GuildId, options: options).FlattenAsync(); return subscriptions.Where(x => x.UserId == guildUser.Id) .GroupBy(x => (x.StartTime, x.EndTime)) - .Select(x => new BoostSubscriptionMetadata(x.Key.StartTime, x.Key.EndTime, x.Count())) + .Select(x => + new BoostSubscriptionMetadata(x.Key.StartTime, x.Key.EndTime, x.Count())) .ToImmutableArray(); } - public static async Task KickAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task KickAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { KickOutGuildMemberParams args = new() { GuildId = user.GuildId, UserId = user.Id }; await client.ApiClient.KickOutGuildMemberAsync(args, options).ConfigureAwait(false); } - public static async Task CreateDMChannelAsync(IUser user, BaseKookClient client, - RequestOptions options) => - RestDMChannel.Create(client, await client.ApiClient.CreateUserChatAsync(user.Id, options).ConfigureAwait(false)); + public static async Task CreateDMChannelAsync(IUser user, + BaseKookClient client, RequestOptions? options) => + RestDMChannel.Create(client, await client.ApiClient + .CreateUserChatAsync(user.Id, options).ConfigureAwait(false)); - public static async Task AddRolesAsync(IGuildUser user, BaseKookClient client, IEnumerable roleIds, RequestOptions options) + public static async Task AddRolesAsync(IGuildUser user, BaseKookClient client, + IEnumerable roleIds, RequestOptions? options) { - IEnumerable args = roleIds.Select(x => new AddOrRemoveRoleParams() - { - GuildId = user.GuildId, RoleId = x, UserId = user.Id - }); - foreach (AddOrRemoveRoleParams arg in args) await client.ApiClient.AddRoleAsync(arg, options).ConfigureAwait(false); + IEnumerable args = roleIds + .Distinct() + .Select(x => new AddOrRemoveRoleParams + { + GuildId = user.GuildId, + RoleId = x, UserId = user.Id + }); + foreach (AddOrRemoveRoleParams arg in args) + await client.ApiClient.AddRoleAsync(arg, options).ConfigureAwait(false); } - public static async Task RemoveRolesAsync(IGuildUser user, BaseKookClient client, IEnumerable roleIds, RequestOptions options) + public static async Task RemoveRolesAsync(IGuildUser user, BaseKookClient client, + IEnumerable roleIds, RequestOptions? options) { - IEnumerable args = roleIds.Select(x => new AddOrRemoveRoleParams() - { - GuildId = user.GuildId, RoleId = x, UserId = user.Id - }); - foreach (AddOrRemoveRoleParams arg in args) await client.ApiClient.RemoveRoleAsync(arg, options).ConfigureAwait(false); + IEnumerable args = roleIds + .Distinct() + .Select(x => new AddOrRemoveRoleParams + { + GuildId = user.GuildId, + RoleId = x, UserId = user.Id + }); + foreach (AddOrRemoveRoleParams arg in args) + await client.ApiClient.RemoveRoleAsync(arg, options).ConfigureAwait(false); } - public static async Task> GetConnectedChannelAsync(IGuildUser user, BaseKookClient client, - RequestOptions options) + public static async Task> GetConnectedChannelAsync( + IGuildUser user, BaseKookClient client, RequestOptions? options) { - IEnumerable channels = await client.ApiClient.GetAudioChannelsUserConnectsAsync(user.GuildId, user.Id, options: options) + IEnumerable channels = await client.ApiClient + .GetAudioChannelsUserConnectsAsync(user.GuildId, user.Id, options: options) .FlattenAsync().ConfigureAwait(false); - return channels.Select(x => RestChannel.Create(client, x) as IVoiceChannel).ToImmutableArray(); + return [..channels.Select(x => RestChannel.Create(client, x)).Cast()]; } - public static async Task StartPlayingAsync(ISelfUser user, BaseKookClient client, IGame game, RequestOptions options) => await client.ApiClient - .BeginActivityAsync(new BeginActivityParams(ActivityType.Game) { Id = game.Id }, options).ConfigureAwait(false); + public static async Task StartPlayingAsync(ISelfUser user, + BaseKookClient client, IGame game, RequestOptions? options) + { + BeginActivityParams args = new() + { + ActivityType = ActivityType.Game, + Id = game.Id + }; + await client.ApiClient.BeginActivityAsync(args, options).ConfigureAwait(false); + } - public static async Task StartPlayingAsync(ISelfUser user, BaseKookClient client, Music music, RequestOptions options) => await client.ApiClient - .BeginActivityAsync( - new BeginActivityParams(ActivityType.Music) { MusicProvider = music.Provider, MusicName = music.Name, Signer = music.Singer }, options) - .ConfigureAwait(false); + public static async Task StartPlayingAsync(ISelfUser user, + BaseKookClient client, Music music, RequestOptions? options) + { + BeginActivityParams args = new() + { + ActivityType = ActivityType.Music, + MusicProvider = music.Provider, + MusicName = music.Name, + Signer = music.Singer + }; + await client.ApiClient.BeginActivityAsync(args, options).ConfigureAwait(false); + } - public static async Task StopPlayingAsync(ISelfUser user, BaseKookClient client, ActivityType type, RequestOptions options) => - await client.ApiClient.EndActivityAsync(new EndGameActivityParams(type), options).ConfigureAwait(false); + public static async Task StopPlayingAsync(ISelfUser user, + BaseKookClient client, ActivityType type, RequestOptions? options) + { + EndGameActivityParams args = new() + { + ActivityType = type + }; + await client.ApiClient.EndActivityAsync(args, options).ConfigureAwait(false); + } - public static async Task GetIntimacyAsync(IUser user, BaseKookClient client, RequestOptions options) + public static async Task GetIntimacyAsync(IUser user, BaseKookClient client, RequestOptions? options) { Intimacy intimacy = await client.ApiClient.GetIntimacyAsync(user.Id, options).ConfigureAwait(false); return RestIntimacy.Create(client, user, intimacy); } - public static async Task UpdateIntimacyAsync(IUser user, BaseKookClient client, Action func, RequestOptions options) + public static async Task UpdateIntimacyAsync(IUser user, BaseKookClient client, + Action func, RequestOptions? options) { - IntimacyProperties properties = new(); + RestIntimacy intimacy = await GetIntimacyAsync(user, client, options).ConfigureAwait(false); + IntimacyProperties properties = new(intimacy.SocialInfo, intimacy.Score); func(properties); UpdateIntimacyValueParams args = new() { - UserId = user.Id, Score = properties.Score, SocialInfo = properties.SocialInfo, ImageId = properties.ImageId + UserId = user.Id, + Score = properties.Score, + SocialInfo = properties.SocialInfo, + ImageId = properties.ImageId }; await client.ApiClient.UpdateIntimacyValueAsync(args, options).ConfigureAwait(false); } - public static async Task BlockAsync(IUser user, BaseKookClient client, RequestOptions options) + public static async Task BlockAsync(IUser user, BaseKookClient client, RequestOptions? options) { - BlockUserParams args = new() { UserId = user.Id }; + BlockUserParams args = new() + { + UserId = user.Id + }; await client.ApiClient.BlockUserAsync(args, options).ConfigureAwait(false); } - public static async Task UnblockAsync(IUser user, BaseKookClient client, RequestOptions options) + public static async Task UnblockAsync(IUser user, BaseKookClient client, RequestOptions? options) { - UnblockUserParams args = new() { UserId = user.Id }; + UnblockUserParams args = new() + { + UserId = user.Id + }; await client.ApiClient.UnblockUserAsync(args, options).ConfigureAwait(false); } - public static async Task RequestFriendAsync(IUser user, BaseKookClient client, RequestOptions options) + public static async Task RequestFriendAsync(IUser user, BaseKookClient client, RequestOptions? options) { RequestFriendParams args = new() { - FullQualification = user.UsernameAndIdentifyNumber(false), Source = RequestFriendSource.FullQualification + FullQualification = user.UsernameAndIdentifyNumber(false), + Source = RequestFriendSource.FullQualification }; await client.ApiClient.RequestFriendAsync(args, options).ConfigureAwait(false); } - public static async Task RequestFriendAsync(IGuildUser user, BaseKookClient client, RequestOptions options) + public static async Task RequestFriendAsync(IGuildUser user, BaseKookClient client, RequestOptions? options) { RequestFriendParams args = new() { - FullQualification = user.UsernameAndIdentifyNumber(false), Source = RequestFriendSource.Guild, GuildId = user.GuildId + FullQualification = user.UsernameAndIdentifyNumber(false), + Source = RequestFriendSource.Guild, GuildId = user.GuildId }; await client.ApiClient.RequestFriendAsync(args, options).ConfigureAwait(false); } - public static async Task RemoveFriendAsync(IUser user, BaseKookClient client, RequestOptions options) + public static async Task RemoveFriendAsync(IUser user, BaseKookClient client, RequestOptions? options) { - RemoveFriendParams args = new() { UserId = user.Id }; + RemoveFriendParams args = new() + { + UserId = user.Id + }; await client.ApiClient.RemoveFriendAsync(args, options).ConfigureAwait(false); } - public static async Task HandleFriendRequestAsync(IFriendRequest request, bool handleResult, BaseKookClient client, RequestOptions options) + public static async Task HandleFriendRequestAsync(IFriendRequest request, + bool handleResult, BaseKookClient client, RequestOptions? options) { - HandleFriendRequestParams args = new() { Id = request.Id, HandleResult = handleResult }; + HandleFriendRequestParams args = new() + { + Id = request.Id, + HandleResult = handleResult + }; await client.ApiClient.HandleFriendRequestAsync(args, options).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs b/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs index 7ebae4da..98359b97 100644 --- a/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs +++ b/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs @@ -1,13 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using Kook.API; using Kook.Net.Converters; -#if NETSTANDARD2_1 || NET5_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -#endif - namespace Kook.Rest; /// @@ -19,7 +16,7 @@ public static class CardJsonExtension { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { new CardConverter(), new ModuleConverter(), new ElementConverter() } + Converters = { CardConverterFactory.Instance } }); /// @@ -28,15 +25,11 @@ public static class CardJsonExtension /// The json string to parse. /// The with populated values. An empty instance if method returns false. /// true if was successfully parsed. false if not. - public static bool TryParseSingle(string json, -#if NETSTANDARD2_1 || NET5_0_OR_GREATER - [NotNullWhen(true)] -#endif - out ICardBuilder builder) + public static bool TryParseSingle(string json, [NotNullWhen(true)] out ICardBuilder? builder) { try { - CardBase model = JsonSerializer.Deserialize(json, _options.Value); + CardBase? model = JsonSerializer.Deserialize(json, _options.Value); if (model is not null) { @@ -44,12 +37,12 @@ public static bool TryParseSingle(string json, return true; } - builder = new CardBuilder(); + builder = null; return false; } catch { - builder = new CardBuilder(); + builder = null; return false; } } @@ -60,15 +53,11 @@ public static bool TryParseSingle(string json, /// The json string to parse. /// A collection of with populated values. An empty instance if method returns false. /// true if was successfully parsed. false if not. - public static bool TryParseMany(string json, -#if NETSTANDARD2_1 || NET5_0_OR_GREATER - [NotNullWhen(true)] -#endif - out IEnumerable builders) + public static bool TryParseMany(string json, [NotNullWhen(true)] out IEnumerable? builders) { try { - IEnumerable models = JsonSerializer.Deserialize>(json, _options.Value); + IEnumerable? models = JsonSerializer.Deserialize>(json, _options.Value); if (models is not null) { @@ -94,12 +83,9 @@ public static bool TryParseMany(string json, /// Thrown if the string passed is not valid json. public static ICardBuilder ParseSingle(string json) { - CardBase model = JsonSerializer.Deserialize(json, _options.Value); - - if (model is not null) - return model.ToEntity().ToBuilder(); - - return new CardBuilder(); + CardBase model = JsonSerializer.Deserialize(json, _options.Value) + ?? throw new JsonException("Unable to parse json into card."); + return model.ToEntity().ToBuilder(); } /// @@ -110,12 +96,9 @@ public static ICardBuilder ParseSingle(string json) /// Thrown if the string passed is not valid json. public static IEnumerable ParseMany(string json) { - IEnumerable models = JsonSerializer.Deserialize>(json, _options.Value); - - if (models is not null) - return models.Select(x => x.ToEntity().ToBuilder()); - - return Enumerable.Empty(); + IEnumerable models = JsonSerializer.Deserialize>(json, _options.Value) + ?? throw new JsonException("Unable to parse json into cards."); + return models.Select(x => x.ToEntity().ToBuilder()); } /// @@ -127,8 +110,8 @@ public static IEnumerable ParseMany(string json) /// The builder to format as Json string. /// Whether to write the json with indents. /// A Json string containing the data from the . - public static string ToJsonString(this ICardBuilder builder, bool writeIndented = true) - => ToJsonString(builder.Build(), writeIndented); + public static string ToJsonString(this ICardBuilder builder, bool writeIndented = true) => + ToJsonString(builder.Build(), writeIndented); /// /// Gets a Json formatted string from an . @@ -146,7 +129,7 @@ public static string ToJsonString(this ICard card, bool writeIndented = true) Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString, WriteIndented = writeIndented, - Converters = { new CardConverter(), new ModuleConverter(), new ElementConverter() }, + Converters = { CardConverterFactory.Instance }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; return JsonSerializer.Serialize(card.ToModel(), options); diff --git a/src/Kook.Net.Rest/Extensions/EntityExtensions.cs b/src/Kook.Net.Rest/Extensions/EntityExtensions.cs index 7d2726d4..2a548784 100644 --- a/src/Kook.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Kook.Net.Rest/Extensions/EntityExtensions.cs @@ -1,13 +1,11 @@ -using System.Collections.Immutable; - namespace Kook.Rest; internal static class EntityExtensions { #region Emotes - public static GuildEmote ToEntity(this API.Emoji model, ulong guildId) - => new(model.Id, + public static GuildEmote ToEntity(this API.Emoji model, ulong guildId) => + new(model.Id, model.Name, model.Type == EmojiType.Animated, guildId, @@ -19,120 +17,83 @@ public static GuildEmote ToEntity(this API.Emoji model, ulong guildId) public static IElement ToEntity(this API.ElementBase model) { - if (model is null) return null; - - return model.Type switch + return model switch { - ElementType.PlainText => (model as API.PlainTextElement).ToEntity(), - ElementType.KMarkdown => (model as API.KMarkdownElement).ToEntity(), - ElementType.Image => (model as API.ImageElement).ToEntity(), - ElementType.Button => (model as API.ButtonElement).ToEntity(), - ElementType.Paragraph => (model as API.ParagraphStruct).ToEntity(), + API.PlainTextElement { Type: ElementType.PlainText } x => x.ToEntity(), + API.KMarkdownElement { Type: ElementType.KMarkdown } x => x.ToEntity(), + API.ImageElement { Type: ElementType.Image } x => x.ToEntity(), + API.ButtonElement { Type: ElementType.Button } x => x.ToEntity(), + API.ParagraphStruct { Type: ElementType.Paragraph } x => x.ToEntity(), _ => throw new ArgumentOutOfRangeException(nameof(model)) }; } - public static PlainTextElement ToEntity(this API.PlainTextElement model) - { - if (model is null) return null; - - return new PlainTextElement(model.Content, model.Emoji); - } - - public static KMarkdownElement ToEntity(this API.KMarkdownElement model) - { - if (model is null) return null; - - return new KMarkdownElement(model.Content); - } - - public static ImageElement ToEntity(this API.ImageElement model) - { - if (model is null) return null; - - return new ImageElement(model.Source, model.Alternative, model.Size, model.Circle); - } + public static PlainTextElement ToEntity(this API.PlainTextElement model) => + new(model.Content, model.Emoji); - public static ButtonElement ToEntity(this API.ButtonElement model) - { - if (model is null) return null; + public static KMarkdownElement ToEntity(this API.KMarkdownElement model) => + new(model.Content); - return new ButtonElement(model.Theme, model.Value, model.Click, model.Text.ToEntity()); - } + public static ImageElement ToEntity(this API.ImageElement model) => + new(model.Source, model.Alternative, model.Size, model.Circle); - public static ParagraphStruct ToEntity(this API.ParagraphStruct model) - { - if (model is null) return null; + public static ButtonElement ToEntity(this API.ButtonElement model) => + new(model.Theme, model.Value, model.Click, model.Text.ToEntity()); - return new ParagraphStruct(model.ColumnCount, model.Fields.Select(e => e.ToEntity()).ToImmutableArray()); - } + public static ParagraphStruct ToEntity(this API.ParagraphStruct model) => + new(model.ColumnCount, [..model.Fields?.Select(e => e.ToEntity()) ?? []]); public static API.ElementBase ToModel(this IElement entity) { - if (entity is null) return null; - - return entity.Type switch + return entity switch { - ElementType.PlainText => (entity as PlainTextElement).ToModel(), - ElementType.KMarkdown => (entity as KMarkdownElement).ToModel(), - ElementType.Image => (entity as ImageElement).ToModel(), - ElementType.Button => (entity as ButtonElement).ToModel(), - ElementType.Paragraph => (entity as ParagraphStruct).ToModel(), + PlainTextElement { Type: ElementType.PlainText } x => x.ToModel(), + KMarkdownElement { Type: ElementType.KMarkdown } x => x.ToModel(), + ImageElement { Type: ElementType.Image } x => x.ToModel(), + ButtonElement { Type: ElementType.Button } x => x.ToModel(), + ParagraphStruct { Type: ElementType.Paragraph } x => x.ToModel(), _ => throw new ArgumentOutOfRangeException(nameof(entity)) }; } - public static API.PlainTextElement ToModel(this PlainTextElement entity) + public static API.PlainTextElement ToModel(this PlainTextElement entity) => new() { - if (entity is null) return null; - - return new API.PlainTextElement { Content = entity.Content, Emoji = entity.Emoji, Type = entity.Type }; - } + Content = entity.Content, + Emoji = entity.Emoji, + Type = entity.Type + }; - public static API.KMarkdownElement ToModel(this KMarkdownElement entity) + public static API.KMarkdownElement ToModel(this KMarkdownElement entity) => new() { - if (entity is null) return null; + Content = entity.Content, + Type = entity.Type + }; - return new API.KMarkdownElement() { Content = entity.Content, Type = entity.Type }; - } - - public static API.ImageElement ToModel(this ImageElement entity) + public static API.ImageElement ToModel(this ImageElement entity) => new() { - if (entity is null) return null; - - return new API.ImageElement() - { - Alternative = entity.Alternative, - Circle = entity.Circle, - Size = entity.Size, - Source = entity.Source, - Type = entity.Type - }; - } + Alternative = entity.Alternative, + Circle = entity.Circle, + Size = entity.Size, + Source = entity.Source, + Type = entity.Type + }; - public static API.ButtonElement ToModel(this ButtonElement entity) + public static API.ButtonElement ToModel(this ButtonElement entity) => new() { - if (entity is null) return null; + Click = entity.Click, + Text = entity.Text.ToModel(), + Theme = entity.Theme, + Type = entity.Type, + Value = entity.Value + }; - return new API.ButtonElement() + public static API.ParagraphStruct ToModel(this ParagraphStruct entity) => + new() { - Click = entity.Click, - Text = entity.Text.ToModel(), - Theme = entity.Theme, - Type = entity.Type, - Value = entity.Value - }; - } - - public static API.ParagraphStruct ToModel(this ParagraphStruct entity) - { - if (entity is null) return null; - - return new API.ParagraphStruct() - { - ColumnCount = entity.ColumnCount, Fields = entity.Fields.Select(e => e.ToModel()).ToArray(), Type = entity.Type + ColumnCount = entity.ColumnCount, + Fields = entity.Fields.Select(e => e.ToModel()).ToArray(), + Type = entity.Type }; - } #endregion @@ -140,266 +101,187 @@ public static API.ParagraphStruct ToModel(this ParagraphStruct entity) public static IModule ToEntity(this API.ModuleBase model) { - if (model is null) return null; - - return model.Type switch + return model switch { - ModuleType.Header => (model as API.HeaderModule).ToEntity(), - ModuleType.Section => (model as API.SectionModule).ToEntity(), - ModuleType.ImageGroup => (model as API.ImageGroupModule).ToEntity(), - ModuleType.Container => (model as API.ContainerModule).ToEntity(), - ModuleType.ActionGroup => (model as API.ActionGroupModule).ToEntity(), - ModuleType.Context => (model as API.ContextModule).ToEntity(), - ModuleType.Divider => (model as API.DividerModule).ToEntity(), - ModuleType.File => (model as API.FileModule).ToEntity(), - ModuleType.Audio => (model as API.AudioModule).ToEntity(), - ModuleType.Video => (model as API.VideoModule).ToEntity(), - ModuleType.Countdown => (model as API.CountdownModule).ToEntity(), - ModuleType.Invite => (model as API.InviteModule).ToEntity(), + API.HeaderModule { Type: ModuleType.Header } x => x.ToEntity(), + API.SectionModule { Type: ModuleType.Section } x => x.ToEntity(), + API.ImageGroupModule { Type: ModuleType.ImageGroup } x => x.ToEntity(), + API.ContainerModule { Type: ModuleType.Container } x => x.ToEntity(), + API.ActionGroupModule { Type: ModuleType.ActionGroup } x => x.ToEntity(), + API.ContextModule { Type: ModuleType.Context } x => x.ToEntity(), + API.DividerModule { Type: ModuleType.Divider } x => x.ToEntity(), + API.FileModule { Type: ModuleType.File } x => x.ToEntity(), + API.AudioModule { Type: ModuleType.Audio } x => x.ToEntity(), + API.VideoModule { Type: ModuleType.Video } x => x.ToEntity(), + API.CountdownModule { Type: ModuleType.Countdown } x => x.ToEntity(), + API.InviteModule { Type: ModuleType.Invite } x => x.ToEntity(), _ => throw new ArgumentOutOfRangeException(nameof(model)) }; } - public static HeaderModule ToEntity(this API.HeaderModule model) - { - if (model is null) return null; + public static HeaderModule ToEntity(this API.HeaderModule model) => + new(model.Text?.ToEntity()); - return new HeaderModule(model.Text.ToEntity()); - } + public static SectionModule ToEntity(this API.SectionModule model) => + new(model.Mode, model.Text?.ToEntity(), model.Accessory?.ToEntity()); - public static SectionModule ToEntity(this API.SectionModule model) - { - if (model is null) return null; + public static ImageGroupModule ToEntity(this API.ImageGroupModule model) => + new([..model.Elements.Select(e => e.ToEntity())]); - return new SectionModule(model.Mode, model.Text.ToEntity(), model.Accessory.ToEntity()); - } + public static ContainerModule ToEntity(this API.ContainerModule model) => + new([..model.Elements.Select(e => e.ToEntity())]); - public static ImageGroupModule ToEntity(this API.ImageGroupModule model) - { - if (model is null) return null; + public static ActionGroupModule ToEntity(this API.ActionGroupModule model) => + new([..model.Elements.Select(e => e.ToEntity())]); - return new ImageGroupModule(model.Elements.Select(e => e.ToEntity()).ToImmutableArray()); - } + public static ContextModule ToEntity(this API.ContextModule model) => + new([..model.Elements?.Select(e => e.ToEntity()) ?? []]); - public static ContainerModule ToEntity(this API.ContainerModule model) - { - if (model is null) return null; + public static DividerModule ToEntity(this API.DividerModule model) => + new(); - return new ContainerModule(model.Elements.Select(e => e.ToEntity()).ToImmutableArray()); - } + public static FileModule ToEntity(this API.FileModule model) => + new(model.Source, model.Title); - public static ActionGroupModule ToEntity(this API.ActionGroupModule model) - { - if (model is null) return null; + public static AudioModule ToEntity(this API.AudioModule model) => + new(model.Source, model.Title, model.Cover); - return new ActionGroupModule(model.Elements.Select(e => e.ToEntity()).ToImmutableArray()); - } + public static VideoModule ToEntity(this API.VideoModule model) => + new(model.Source, model.Title); - public static ContextModule ToEntity(this API.ContextModule model) - { - if (model is null) return null; + public static CountdownModule ToEntity(this API.CountdownModule model) => + new(model.Mode, model.EndTime, model.StartTime); - return new ContextModule(model.Elements.Select(e => e.ToEntity()).ToImmutableArray()); - } - - public static DividerModule ToEntity(this API.DividerModule model) - { - if (model is null) return null; - - return new DividerModule(); - } - - public static FileModule ToEntity(this API.FileModule model) - { - if (model is null) return null; - - return new FileModule(model.Source, model.Title); - } - - public static AudioModule ToEntity(this API.AudioModule model) - { - if (model is null) return null; - - return new AudioModule(model.Source, model.Title, model.Cover); - } - - public static VideoModule ToEntity(this API.VideoModule model) - { - if (model is null) return null; - - return new VideoModule(model.Source, model.Title); - } - - public static CountdownModule ToEntity(this API.CountdownModule model) - { - if (model is null) return null; - - return new CountdownModule(model.Mode, model.EndTime, model.StartTime); - } - - public static InviteModule ToEntity(this API.InviteModule model) - { - if (model is null) return null; - - return new InviteModule(model.Code); - } + public static InviteModule ToEntity(this API.InviteModule model) => + new(model.Code); public static API.ModuleBase ToModel(this IModule entity) { - if (entity is null) return null; - - return entity.Type switch + return entity switch { - ModuleType.Header => (entity as HeaderModule).ToModel(), - ModuleType.Section => (entity as SectionModule).ToModel(), - ModuleType.ImageGroup => (entity as ImageGroupModule).ToModel(), - ModuleType.Container => (entity as ContainerModule).ToModel(), - ModuleType.ActionGroup => (entity as ActionGroupModule).ToModel(), - ModuleType.Context => (entity as ContextModule).ToModel(), - ModuleType.Divider => (entity as DividerModule).ToModel(), - ModuleType.File => (entity as FileModule).ToModel(), - ModuleType.Audio => (entity as AudioModule).ToModel(), - ModuleType.Video => (entity as VideoModule).ToModel(), - ModuleType.Countdown => (entity as CountdownModule).ToModel(), - ModuleType.Invite => (entity as InviteModule).ToModel(), + HeaderModule { Type: ModuleType.Header } x => x.ToModel(), + SectionModule { Type: ModuleType.Section } x => x.ToModel(), + ImageGroupModule { Type: ModuleType.ImageGroup } x => x.ToModel(), + ContainerModule { Type: ModuleType.Container } x => x.ToModel(), + ActionGroupModule { Type: ModuleType.ActionGroup } x => x.ToModel(), + ContextModule { Type: ModuleType.Context } x => x.ToModel(), + DividerModule { Type: ModuleType.Divider } x => x.ToModel(), + FileModule { Type: ModuleType.File } x => x.ToModel(), + AudioModule { Type: ModuleType.Audio } x => x.ToModel(), + VideoModule { Type: ModuleType.Video } x => x.ToModel(), + CountdownModule { Type: ModuleType.Countdown } x => x.ToModel(), + InviteModule { Type: ModuleType.Invite } x => x.ToModel(), _ => throw new ArgumentOutOfRangeException(nameof(entity)) }; } - public static API.HeaderModule ToModel(this HeaderModule entity) + public static API.HeaderModule ToModel(this HeaderModule entity) => new() { - if (entity is null) return null; - - return new API.HeaderModule() { Text = entity.Text.ToModel(), Type = entity.Type }; - } + Text = entity.Text?.ToModel(), + Type = entity.Type + }; - public static API.SectionModule ToModel(this SectionModule entity) + public static API.SectionModule ToModel(this SectionModule entity) => new() { - if (entity is null) return null; - - return new API.SectionModule() - { - Accessory = entity.Accessory.ToModel(), Mode = entity.Mode, Text = entity.Text.ToModel(), Type = entity.Type - }; - } + Accessory = entity.Accessory?.ToModel(), + Mode = entity.Mode, + Text = entity.Text?.ToModel(), + Type = entity.Type + }; - public static API.ImageGroupModule ToModel(this ImageGroupModule entity) + public static API.ImageGroupModule ToModel(this ImageGroupModule entity) => new() { - if (entity is null) return null; + Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), + Type = entity.Type + }; - return new API.ImageGroupModule() { Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), Type = entity.Type }; - } - - public static API.ContainerModule ToModel(this ContainerModule entity) + public static API.ContainerModule ToModel(this ContainerModule entity) => new() { - if (entity is null) return null; - - return new API.ContainerModule() { Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), Type = entity.Type }; - } + Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), + Type = entity.Type + }; - public static API.ActionGroupModule ToModel(this ActionGroupModule entity) + public static API.ActionGroupModule ToModel(this ActionGroupModule entity) => new() { - if (entity is null) return null; + Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), + Type = entity.Type + }; - return new API.ActionGroupModule() { Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), Type = entity.Type }; - } - - public static API.ContextModule ToModel(this ContextModule entity) + public static API.ContextModule ToModel(this ContextModule entity) => new() { - if (entity is null) return null; - - return new API.ContextModule() { Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), Type = entity.Type }; - } + Elements = entity.Elements.Select(e => e.ToModel()).ToArray(), + Type = entity.Type + }; - public static API.DividerModule ToModel(this DividerModule entity) + public static API.DividerModule ToModel(this DividerModule entity) => new() { - if (entity is null) return null; + Type = entity.Type + }; - return new API.DividerModule() { Type = entity.Type }; - } - - public static API.FileModule ToModel(this FileModule entity) + public static API.FileModule ToModel(this FileModule entity) => new() { - if (entity is null) return null; - - return new API.FileModule() { Source = entity.Source, Title = entity.Title, Type = entity.Type }; - } + Source = entity.Source, + Title = entity.Title, + Type = entity.Type + }; - public static API.AudioModule ToModel(this AudioModule entity) + public static API.AudioModule ToModel(this AudioModule entity) => new() { - if (entity is null) return null; + Cover = entity.Cover, + Source = entity.Source, + Title = entity.Title, + Type = entity.Type + }; - return new API.AudioModule() { Cover = entity.Cover, Source = entity.Source, Title = entity.Title, Type = entity.Type }; - } - - public static API.VideoModule ToModel(this VideoModule entity) + public static API.VideoModule ToModel(this VideoModule entity) => new() { - if (entity is null) return null; - - return new API.VideoModule() { Source = entity.Source, Title = entity.Title, Type = entity.Type }; - } + Source = entity.Source, + Title = entity.Title, + Type = entity.Type + }; - public static API.CountdownModule ToModel(this CountdownModule entity) + public static API.CountdownModule ToModel(this CountdownModule entity) => new() { - if (entity is null) return null; - - return new API.CountdownModule() { Mode = entity.Mode, EndTime = entity.EndTime, StartTime = entity.StartTime, Type = entity.Type }; - } + Mode = entity.Mode, + EndTime = entity.EndTime, + StartTime = entity.StartTime, + Type = entity.Type + }; - public static API.InviteModule ToModel(this InviteModule entity) + public static API.InviteModule ToModel(this InviteModule entity) => new() { - if (entity is null) return null; - - return new API.InviteModule() { Code = entity.Code, Type = entity.Type }; - } + Code = entity.Code, + Type = entity.Type + }; #endregion #region Cards - public static ICard ToEntity(this API.CardBase model) + public static ICard ToEntity(this API.CardBase model) => model switch { - if (model is null) return null; + API.Card { Type: CardType.Card } x => x.ToEntity(), + _ => throw new ArgumentOutOfRangeException(nameof(model)) + }; - return model.Type switch - { - CardType.Card => (model as API.Card).ToEntity(), - _ => throw new ArgumentOutOfRangeException(nameof(model)) - }; - } + public static Card ToEntity(this API.Card model) => + new(model.Theme ?? CardTheme.Primary, model.Size ?? CardSize.Large, model.Color, [..model.Modules.Select(m => m.ToEntity())]); - public static Card ToEntity(this API.Card model) + public static API.CardBase ToModel(this ICard entity) => entity switch { - if (model is null) return null; - - return new Card(model.Theme, model.Size, model.Color, model.Modules.Select(m => m.ToEntity()).ToImmutableArray()); - } + Card { Type: CardType.Card } x => x.ToModel(), + _ => throw new ArgumentOutOfRangeException(nameof(entity)) + }; - public static API.CardBase ToModel(this ICard entity) + public static API.Card ToModel(this Card entity) => new() { - if (entity is null) return null; - - return entity.Type switch - { - CardType.Card => (entity as Card).ToModel(), - _ => throw new ArgumentOutOfRangeException(nameof(entity)) - }; - } - - public static API.Card ToModel(this Card entity) - { - if (entity is null) return null; - - return new API.Card() - { - Theme = entity.Theme, - Color = entity.Color, - Size = entity.Size, - Modules = entity.Modules.Select(m => m.ToModel()).ToArray(), - Type = entity.Type - }; - } + Theme = entity.Theme, + Color = entity.Color, + Size = entity.Size, + Modules = entity.Modules.Select(m => m.ToModel()).ToArray(), + Type = entity.Type + }; #endregion @@ -407,14 +289,14 @@ public static API.Card ToModel(this Card entity) public static IEmbed ToEntity(this API.EmbedBase model) { - if (model is null) return null; - - return model.Type switch + return model switch { - EmbedType.Link => (model as API.LinkEmbed).ToEntity(), - EmbedType.Image => (model as API.ImageEmbed).ToEntity(), - EmbedType.BilibiliVideo => (model as API.BilibiliVideoEmbed).ToEntity(), - _ => (model as API.NotImplementedEmbed).ToNotImplementedEntity() + API.LinkEmbed { Type: EmbedType.Link } x => x.ToEntity(), + API.ImageEmbed { Type: EmbedType.Image } x => x.ToEntity(), + API.BilibiliVideoEmbed { Type: EmbedType.BilibiliVideo } x => x.ToEntity(), + API.CardEmbed { Type: EmbedType.Card } x => x.ToEntity(), + API.NotImplementedEmbed { Type: EmbedType.NotImplemented } x => x.ToNotImplementedEntity(), + _ => throw new ArgumentOutOfRangeException(nameof(model)) }; } @@ -427,6 +309,9 @@ public static BilibiliVideoEmbed ToEntity(this API.BilibiliVideoEmbed model) => new(model.Url, model.OriginUrl, model.BvNumber, model.IframePath, TimeSpan.FromSeconds(model.Duration), model.Title, model.Cover); + public static CardEmbed ToEntity(this API.CardEmbed model) => + new(new Card(model.Theme ?? CardTheme.Primary, model.Size ?? CardSize.Large, model.Color, [..model.Modules.Select(m => m.ToEntity())])); + public static NotImplementedEmbed ToNotImplementedEntity(this API.NotImplementedEmbed model) => new(model.RawType, model.RawJsonNode); @@ -436,12 +321,11 @@ public static NotImplementedEmbed ToNotImplementedEntity(this API.NotImplemented public static IPokeResource ToEntity(this API.PokeResourceBase model) { - if (model is null) return null; - - return model.Type switch + return model switch { - PokeResourceType.ImageAnimation => (model as API.ImageAnimationPokeResource).ToEntity(), - _ => (model as API.NotImplementedPokeResource).ToNotImplementedEntity() + API.ImageAnimationPokeResource { Type: PokeResourceType.ImageAnimation } x => x.ToEntity(), + API.NotImplementedPokeResource { Type: PokeResourceType.NotImplemented } x => x.ToNotImplementedEntity(), + _ => throw new ArgumentOutOfRangeException(nameof(model)) }; } @@ -449,15 +333,15 @@ public static NotImplementedPokeResource ToNotImplementedEntity(this API.NotImpl new(model.RawType, model.RawJsonNode); public static ImageAnimationPokeResource ToEntity(this API.ImageAnimationPokeResource model) => - new(new Dictionary() { ["webp"] = model.WebP, ["pag"] = model.PAG, ["gif"] = model.GIF }, - TimeSpan.FromMilliseconds(model.Duration), model.Width, model.Height, model.Percent / 100D); + new(new Dictionary { ["webp"] = model.WebP, ["pag"] = model.PAG, ["gif"] = model.GIF }, + TimeSpan.FromMilliseconds(model.Duration), model.Width, model.Height, model.Percent / 100M); #endregion #region Overwrites - public static UserPermissionOverwrite ToEntity(this API.UserPermissionOverwrite model) => - new(RestUser.Create(null, model.User), new OverwritePermissions(model.Allow, model.Deny)); + public static UserPermissionOverwrite ToEntity(this API.UserPermissionOverwrite model, BaseKookClient kook) => + new(RestUser.Create(kook, model.User), new OverwritePermissions(model.Allow, model.Deny)); public static RolePermissionOverwrite ToEntity(this API.RolePermissionOverwrite model) => new(model.RoleId, new OverwritePermissions(model.Allow, model.Deny)); @@ -468,8 +352,6 @@ public static RolePermissionOverwrite ToEntity(this API.RolePermissionOverwrite public static RecommendInfo ToEntity(this API.RecommendInfo model) { - if (model is null) return null; - return RecommendInfo.Create(model); } @@ -479,8 +361,6 @@ public static RecommendInfo ToEntity(this API.RecommendInfo model) public static UserTag ToEntity(this API.UserTag model) { - if (model is null) return null; - return UserTag.Create(model.Color, model.BackgroundColor, model.Text); } @@ -490,8 +370,6 @@ public static UserTag ToEntity(this API.UserTag model) public static Nameplate ToEntity(this API.Nameplate model) { - if (model is null) return null; - return Nameplate.Create(model.Name, model.Type, model.Icon, model.Tips); } diff --git a/src/Kook.Net.Rest/Kook.Net.Rest.csproj b/src/Kook.Net.Rest/Kook.Net.Rest.csproj index cf31baa3..2cb001a7 100644 --- a/src/Kook.Net.Rest/Kook.Net.Rest.csproj +++ b/src/Kook.Net.Rest/Kook.Net.Rest.csproj @@ -10,12 +10,12 @@ - TRACE_REST + DEBUG_REST - + diff --git a/src/Kook.Net.Rest/KookRestApiClient.cs b/src/Kook.Net.Rest/KookRestApiClient.cs index a18f0ce7..aae42965 100644 --- a/src/Kook.Net.Rest/KookRestApiClient.cs +++ b/src/Kook.Net.Rest/KookRestApiClient.cs @@ -1,12 +1,10 @@ #if NET462 -using System.Net; using System.Net.Http; -#else -using System.Web; #endif using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; using System.Runtime.CompilerServices; @@ -16,6 +14,7 @@ using System.Text.Json.Serialization; using Kook.API.Rest; using Kook.Net; +using Kook.Net.Converters; using Kook.Net.Queue; using Kook.Net.Rest; @@ -40,7 +39,7 @@ public event Func SentRequest private readonly RestClientProvider _restClientProvider; protected bool _isDisposed; - private CancellationTokenSource _loginCancellationToken; + private CancellationTokenSource? _loginCancellationToken; public RetryMode DefaultRetryMode { get; } public string UserAgent { get; } @@ -48,14 +47,14 @@ public event Func SentRequest internal RequestQueue RequestQueue { get; } public LoginState LoginState { get; private set; } public TokenType AuthTokenType { get; private set; } - internal string AuthToken { get; private set; } + internal string? AuthToken { get; private set; } internal IRestClient RestClient { get; private set; } internal ulong? CurrentUserId { get; set; } - internal Func DefaultRatelimitCallback { get; set; } + internal Func? DefaultRatelimitCallback { get; set; } - public KookRestApiClient(RestClientProvider restClientProvider, string userAgent, string acceptLanguage = null, - RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializerOptions serializerOptions = null, - Func defaultRatelimitCallback = null) + public KookRestApiClient(RestClientProvider restClientProvider, string userAgent, string acceptLanguage, + RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializerOptions? serializerOptions = null, + Func? defaultRatelimitCallback = null) { _restClientProvider = restClientProvider; UserAgent = userAgent; @@ -63,7 +62,9 @@ public KookRestApiClient(RestClientProvider restClientProvider, string userAgent _serializerOptions = serializerOptions ?? new JsonSerializerOptions { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = { new CardConverterFactory() } }; DefaultRatelimitCallback = defaultRatelimitCallback; @@ -73,16 +74,17 @@ public KookRestApiClient(RestClientProvider restClientProvider, string userAgent SetAcceptLanguage(acceptLanguage); } + [MemberNotNull(nameof(RestClient))] internal void SetBaseUrl(string baseUrl) { RestClient?.Dispose(); RestClient = _restClientProvider(baseUrl); RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); - RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); } - internal void SetAcceptLanguage(string acceptLanguage) => RestClient.SetHeader("accept-language", acceptLanguage); + internal void SetAcceptLanguage(string acceptLanguage) => + RestClient.SetHeader("accept-language", acceptLanguage); internal static string GetPrefixedToken(TokenType tokenType, string token) => tokenType switch @@ -128,7 +130,7 @@ private async Task LoginInternalAsync(TokenType tokenType, string token) RestClient.SetCancellationToken(_loginCancellationToken.Token); AuthTokenType = tokenType; - AuthToken = token?.TrimEnd(); + AuthToken = token.TrimEnd(); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; @@ -169,29 +171,31 @@ private async Task LogoutInternalAsync() // ignored } - await DisconnectInternalAsync(null).ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancellationTokenAsync(CancellationToken.None).ConfigureAwait(false); RestClient.SetCancellationToken(CancellationToken.None); - // CurrentUserId = null; + CurrentUserId = null; LoginState = LoginState.LoggedOut; } internal virtual Task ConnectInternalAsync() => Task.Delay(0); - internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); + internal virtual Task DisconnectInternalAsync(Exception? ex = null) => Task.CompletedTask; #endregion #region Core internal Task SendAsync(HttpMethod method, Expression> endpointExpr, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) => + SendAsync(method, GetEndpoint(endpointExpr), + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); - public async Task SendAsync(HttpMethod method, string endpoint, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + public async Task SendAsync(HttpMethod method, string endpoint, BucketId? bucketId = null, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null) { options ??= new RequestOptions(); options.BucketId = bucketId; @@ -201,28 +205,32 @@ public async Task SendAsync(HttpMethod method, string endpoint, } internal Task SendJsonAsync(HttpMethod method, Expression> endpointExpr, object payload, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) => + SendJsonAsync(method, GetEndpoint(endpointExpr), payload, + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); - public async Task SendJsonAsync(HttpMethod method, string endpoint, object payload, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + public async Task SendJsonAsync(HttpMethod method, string endpoint, object payload, BucketId? bucketId = null, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null) { options ??= new RequestOptions(); options.BucketId = bucketId; - string json = payload != null ? SerializeJson(payload) : null; + string? json = SerializeJson(payload); JsonRestRequest request = new(RestClient, method, endpoint, json, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } - internal Task SendMultipartAsync(HttpMethod method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, - BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, - options); + internal Task SendMultipartAsync(HttpMethod method, Expression> endpointExpr, + IReadOnlyDictionary multipartArgs, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) => + SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); - public async Task SendMultipartAsync(HttpMethod method, string endpoint, IReadOnlyDictionary multipartArgs, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + public async Task SendMultipartAsync(HttpMethod method, string endpoint, + IReadOnlyDictionary multipartArgs, BucketId? bucketId = null, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions? options = null) { options ??= new RequestOptions(); options.BucketId = bucketId; @@ -231,73 +239,92 @@ public async Task SendMultipartAsync(HttpMethod method, string endpoint, IReadOn await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } - internal async Task SendAsync(HttpMethod method, Expression> endpointExpr, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null, - bool bypassDeserialization = false) where TResponse : class - => await SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, - bypassDeserialization, options); + internal async Task SendAsync(HttpMethod method, Expression> endpointExpr, + BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) + where TResponse : class => + await SendAsync(method, GetEndpoint(endpointExpr), + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, bypassDeserialization, options); - internal async Task SendAsync(HttpMethod method, Expression> endpointExpr, - TArg1 arg1, TArg2 arg2, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null, - bool bypassDeserialization = false) where TResponse : class - => await SendAsync(method, GetEndpoint(endpointExpr, arg1, arg2), GetBucketId(method, ids, endpointExpr, arg1, arg2, funcName), - clientBucket, bypassDeserialization, options); + internal async Task SendAsync(HttpMethod method, + Expression> endpointExpr, TArg1 arg1, TArg2 arg2, + BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) + where TResponse : class => + await SendAsync(method, GetEndpoint(endpointExpr, arg1, arg2), + GetBucketId(method, ids, endpointExpr, arg1, arg2, funcName), clientBucket, bypassDeserialization, options); public async Task SendAsync(HttpMethod method, string endpoint, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, bool bypassDeserialization = false, - RequestOptions options = null) where TResponse : class + BucketId? bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null) + where TResponse : class { options ??= new RequestOptions(); options.BucketId = bucketId; RestRequest request = new(RestClient, method, endpoint, options); Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); - return bypassDeserialization ? response as TResponse : await DeserializeJsonAsync(response).ConfigureAwait(false); + return bypassDeserialization && response is TResponse responseObj + ? responseObj + : await DeserializeJsonAsync(response).ConfigureAwait(false); } - internal async Task SendJsonAsync(HttpMethod method, Expression> endpointExpr, object payload, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null, - bool bypassDeserialization = false) where TResponse : class - => await SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, - bypassDeserialization, options); + internal async Task SendJsonAsync(HttpMethod method, + Expression> endpointExpr, object payload, + BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) + where TResponse : class => + await SendJsonAsync(method, GetEndpoint(endpointExpr), payload, + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, bypassDeserialization, options); public async Task SendJsonAsync(HttpMethod method, string endpoint, object payload, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, bool bypassDeserialization = false, - RequestOptions options = null) where TResponse : class + BucketId? bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null) + where TResponse : class { options ??= new RequestOptions(); options.BucketId = bucketId; - string json = payload != null ? SerializeJson(payload) : null; - + string json = SerializeJson(payload); JsonRestRequest request = new(RestClient, method, endpoint, json, options); Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); - return bypassDeserialization ? response as TResponse : await DeserializeJsonAsync(response).ConfigureAwait(false); - } - - internal Task SendMultipartAsync(HttpMethod method, Expression> endpointExpr, - IReadOnlyDictionary multipartArgs, BucketIds ids, - ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null, - bool bypassDeserialization = false) where TResponse : class - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), - clientBucket, bypassDeserialization, options); - - public async Task SendMultipartAsync(HttpMethod method, string endpoint, IReadOnlyDictionary multipartArgs, - BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, bool bypassDeserialization = false, - RequestOptions options = null) where TResponse : class + return bypassDeserialization && response is TResponse responseObj + ? responseObj + : await DeserializeJsonAsync(response).ConfigureAwait(false); + } + + internal Task SendMultipartAsync(HttpMethod method, + Expression> endpointExpr, IReadOnlyDictionary multipartArgs, + BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null, + [CallerMemberName] string? funcName = null) + where TResponse : class => + SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, + GetBucketId(method, ids, endpointExpr, funcName), clientBucket, bypassDeserialization, options); + + public async Task SendMultipartAsync(HttpMethod method, + string endpoint, IReadOnlyDictionary multipartArgs, + BucketId? bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + bool bypassDeserialization = false, RequestOptions? options = null) + where TResponse : class { options ??= new RequestOptions(); options.BucketId = bucketId; MultipartRestRequest request = new(RestClient, method, endpoint, multipartArgs, options); Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); - return bypassDeserialization ? response as TResponse : await DeserializeJsonAsync(response).ConfigureAwait(false); + return bypassDeserialization && response is TResponse responseObj + ? responseObj + : await DeserializeJsonAsync(response).ConfigureAwait(false); } private async Task SendInternalAsync(HttpMethod method, string endpoint, RestRequest request) { - if (!request.Options.IgnoreState) CheckState(); + if (!request.Options.IgnoreState) + CheckState(); request.Options.RetryMode ??= DefaultRetryMode; request.Options.RatelimitCallback ??= DefaultRatelimitCallback; @@ -314,17 +341,20 @@ private async Task SendInternalAsync(HttpMethod method, string endpoint, internal async IAsyncEnumerable> SendPagedAsync(HttpMethod method, Expression> endpointExpr, - BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, PageMeta pageMeta = null, RequestOptions options = null) + BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, + PageMeta? pageMeta = null, RequestOptions? options = null) where T : class { pageMeta ??= PageMeta.Default; while (pageMeta.Page <= pageMeta.PageTotal) { - PagedResponseBase pagedResp = await SendAsync, int, int>( + PagedResponseBase? pagedResp = await SendAsync, int, int>( method, endpointExpr, pageMeta.PageSize, pageMeta.Page, - ids, clientBucket, options) + ids, clientBucket, false, options) .ConfigureAwait(false); + if (pagedResp is null) + yield break; pageMeta = pagedResp.Meta; pageMeta.Page++; yield return pagedResp.Items; @@ -335,17 +365,17 @@ internal async IAsyncEnumerable> SendPagedAsync(HttpMe #region Guilds - public async Task> ListGuildsAsync(RequestOptions options = null) + public async Task>ListGuildsAsync(RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); return await SendAsync>(HttpMethod.Get, - () => $"guild/index", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + () => $"guild/index", ids, ClientBucketType.SendEdit, false, options).ConfigureAwait(false); } public IAsyncEnumerable> GetGuildsAsync(int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, - RequestOptions options = null) + RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); @@ -355,71 +385,74 @@ public IAsyncEnumerable> GetGuildsAsync(int limit = K ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) + public async Task GetGuildAsync(ulong guildId, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); return await SendAsync(HttpMethod.Get, - () => $"guild/view?guild_id={guildId}", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + () => $"guild/view?guild_id={guildId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async ValueTask GetGuildMemberCountAsync(ulong guildId, RequestOptions options = null) + public async ValueTask GetGuildMemberCountAsync(ulong guildId, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - GetGuildMemberCountResponse response = await SendAsync(HttpMethod.Get, - () => $"guild/user-list?guild_id={guildId}", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); - return response.UserCount; + GetGuildMemberCountResponse? response = await SendAsync(HttpMethod.Get, + () => $"guild/user-list?guild_id={guildId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); + return response?.UserCount; } - public IAsyncEnumerable> GetGuildMembersAsync(ulong guildId, Action func = null, - int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions options = null) + public IAsyncEnumerable> GetGuildMembersAsync(ulong guildId, + Action? func = null, + int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.GreaterThan(limit, 0, nameof(limit)); Preconditions.AtMost(limit, KookConfig.MaxUsersPerBatch, nameof(limit)); options = RequestOptions.CreateOrClone(options); - string extendedQuery = string.Empty; + StringBuilder builder = new(); if (func is not null) { SearchGuildMemberProperties properties = new(); func(properties); - if (!string.IsNullOrWhiteSpace(properties.SearchName)) extendedQuery += $"&search={Uri.EscapeDataString(properties.SearchName)}"; - - if (properties.RoleId.HasValue) extendedQuery += $"&role_id={properties.RoleId.Value}"; - + if (!string.IsNullOrWhiteSpace(properties.SearchName)) + builder.Append($"&search={UrlEncode(properties.SearchName)}"); + if (properties.RoleId.HasValue) + builder.Append($"&role_id={properties.RoleId.Value}"); if (properties.IsMobileVerified.HasValue) - extendedQuery += $"&mobile_verified={properties.IsMobileVerified.Value switch { true => 1, false => 0 }}"; - - if (properties.SortedByActiveTime.HasValue) extendedQuery += $"&active_time={(int)properties.SortedByActiveTime.Value}"; - - if (properties.SortedByJoinTime.HasValue) extendedQuery += $"&joined_at={(int)properties.SortedByJoinTime.Value}"; + builder.Append($"&mobile_verified={properties.IsMobileVerified.Value switch { true => 1, false => 0 }}"); + if (properties.SortedByActiveTime.HasValue) + builder.Append($"&active_time={(int)properties.SortedByActiveTime.Value}"); + if (properties.SortedByJoinTime.HasValue) + builder.Append($"&joined_at={(int)properties.SortedByJoinTime.Value}"); } BucketIds ids = new(guildId); return SendPagedAsync(HttpMethod.Get, - (pageSize, page) => $"guild/user-list?guild_id={guildId}&page_size={pageSize}&page={page}{extendedQuery}", + (pageSize, page) => $"guild/user-list?guild_id={guildId}&page_size={pageSize}&page={page}{builder}", ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task ModifyGuildMemberNicknameAsync(ModifyGuildMemberNicknameParams args, RequestOptions options = null) + public async Task ModifyGuildMemberNicknameAsync(ModifyGuildMemberNicknameParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); if (args.Nickname?.Length > KookConfig.MaxNicknameSize) throw new ArgumentException($"Nickname is too long, length must be less or equal to {KookConfig.MaxNicknameSize}.", nameof(args.Nickname)); - if (args.Nickname?.Length < KookConfig.MinNicknameSize) throw new ArgumentException($"Nickname is too short, length must be more or equal to {KookConfig.MinNicknameSize}.", nameof(args.Nickname)); - if (args.UserId is not null) Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); + if (args.UserId is not null) + Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); options = RequestOptions.CreateOrClone(options); @@ -427,7 +460,7 @@ public async Task ModifyGuildMemberNicknameAsync(ModifyGuildMemberNicknameParams await SendJsonAsync(HttpMethod.Post, () => $"guild/nickname", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } - public async Task LeaveGuildAsync(LeaveGuildParams args, RequestOptions options = null) + public async Task LeaveGuildAsync(LeaveGuildParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); @@ -437,7 +470,7 @@ public async Task LeaveGuildAsync(LeaveGuildParams args, RequestOptions options await SendJsonAsync(HttpMethod.Post, () => $"guild/leave", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } - public async Task KickOutGuildMemberAsync(KickOutGuildMemberParams args, RequestOptions options = null) + public async Task KickOutGuildMemberAsync(KickOutGuildMemberParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); @@ -448,17 +481,18 @@ public async Task KickOutGuildMemberAsync(KickOutGuildMemberParams args, Request await SendJsonAsync(HttpMethod.Post, () => $"guild/kickout", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } - public async Task GetGuildMutedDeafenedUsersAsync(ulong guildId, RequestOptions options = null) + public async Task GetGuildMutedDeafenedUsersAsync(ulong guildId, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - return await SendAsync(HttpMethod.Get, () => $"guild-mute/list?guild_id={guildId}&return_type=detail", ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Get, + () => $"guild-mute/list?guild_id={guildId}&return_type=detail", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task CreateGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams args, RequestOptions options = null) + public async Task CreateGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); @@ -469,7 +503,7 @@ public async Task CreateGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams arg await SendJsonAsync(HttpMethod.Post, () => $"guild-mute/create", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } - public async Task RemoveGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams args, RequestOptions options = null) + public async Task RemoveGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); @@ -482,18 +516,19 @@ public async Task RemoveGuildMuteDeafAsync(CreateOrRemoveGuildMuteDeafParams arg public IAsyncEnumerable> GetGuildBoostSubscriptionsAsync(ulong guildId, DateTimeOffset? since = null, DateTimeOffset? until = null, - int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions options = null) + int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); string query = $"guild_id={guildId}"; - if (since.HasValue) query += $"&start_time={since.Value.ToUnixTimeSeconds()}"; - - if (until.HasValue) query += $"&end_time={until.Value.ToUnixTimeSeconds()}"; - + if (since.HasValue) + query += $"&start_time={since.Value.ToUnixTimeSeconds()}"; + if (until.HasValue) + query += $"&end_time={until.Value.ToUnixTimeSeconds()}"; options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - return SendPagedAsync(HttpMethod.Get, (pageSize, page) => $"guild-boost/history?{query}&page_size={pageSize}&page={page}", + return SendPagedAsync(HttpMethod.Get, + (pageSize, page) => $"guild-boost/history?{query}&page_size={pageSize}&page={page}", ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } @@ -501,8 +536,8 @@ public IAsyncEnumerable> GetGuildBoostSub #region Channels - public IAsyncEnumerable> GetGuildChannelsAsync(ulong guildId, int limit = KookConfig.MaxItemsPerBatchByDefault, - int fromPage = 1, RequestOptions options = null) + public IAsyncEnumerable> GetGuildChannelsAsync(ulong guildId, + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -513,73 +548,85 @@ public IAsyncEnumerable> GetGuildChannelsAsync(ulon ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task GetGuildChannelAsync(ulong channelId, RequestOptions options = null) + public async Task GetGuildChannelAsync(ulong channelId, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: channelId); - return await SendAsync(HttpMethod.Get, () => $"channel/view?target_id={channelId}", ids, ClientBucketType.SendEdit, options) + return await SendAsync(HttpMethod.Get, + () => $"channel/view?target_id={channelId}", + ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) + public async Task CreateGuildChannelAsync(CreateGuildChannelParams args, RequestOptions? options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - Preconditions.AtMost(args.Name?.Length, 100, nameof(args.Name)); - + Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); + Preconditions.AtLeast(args.LimitAmount, 0, nameof(args.LimitAmount)); + Preconditions.AtMost(args.LimitAmount, 99, nameof(args.LimitAmount)); options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(channelId: channelId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel/update", args, ids, ClientBucketType.SendEdit, options) + BucketIds ids = new(args.GuildId); + return await SendJsonAsync(HttpMethod.Post, + () => $"channel/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.AtMost(args.Name?.Length, 100, nameof(args.Name)); - Preconditions.AtMost(args.Topic?.Length, 500, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: channelId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel/update", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"channel/update", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - Preconditions.AtMost(args.Name?.Length, 99, nameof(args.Name)); + Preconditions.AtMost(args.Name?.Length, 100, nameof(args.Name)); + Preconditions.AtMost(args.Topic?.Length, 500, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: channelId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel/update", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"channel/update", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task CreateGuildChannelAsync(CreateGuildChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions? options = null) { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); - Preconditions.AtLeast(args.LimitAmount, 0, nameof(args.LimitAmount)); - Preconditions.AtMost(args.LimitAmount, 99, nameof(args.LimitAmount)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + Preconditions.AtMost(args.Name?.Length, 100, nameof(args.Name)); + Preconditions.AtMost(args.Topic?.Length, 500, nameof(args.Name)); + Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit)); + Preconditions.AtMost(args.Password?.Length, 12, nameof(args.Password)); + if (args.Password?.All(char.IsNumber) is false) + throw new ArgumentException("Password must contain at least one non-numeric character.", + nameof(args.Password)); + options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(args.GuildId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel/create", args, ids, ClientBucketType.SendEdit, options) + BucketIds ids = new(channelId: channelId); + return await SendJsonAsync(HttpMethod.Post, + () => $"channel/update", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task DeleteGuildChannelAsync(DeleteGuildChannelParams args, RequestOptions options = null) + public async Task DeleteGuildChannelAsync(DeleteGuildChannelParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); @@ -589,42 +636,45 @@ public async Task DeleteGuildChannelAsync(DeleteGuildChannelParams args, Request await SendJsonAsync(HttpMethod.Post, () => $"channel/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } - public async Task MoveUsersAsync(MoveUsersParams args, RequestOptions options = null) + public async Task>GetConnectedUsersAsync(ulong channelId, RequestOptions? options = null) { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(channelId: args.ChannelId); - await SendJsonAsync(HttpMethod.Post, () => $"channel/move-user", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + BucketIds ids = new(channelId: channelId); + return await SendAsync>(HttpMethod.Get, + () => $"channel/user-list?channel_id={channelId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task> GetConnectedUsersAsync(ulong channelId, RequestOptions options = null) + public async Task MoveUsersAsync(MoveUsersParams args, RequestOptions? options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(channelId: channelId); - return await SendAsync>(HttpMethod.Get, () => $"channel/user-list?channel_id={channelId}", ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + BucketIds ids = new(channelId: args.ChannelId); + await SendJsonAsync(HttpMethod.Post, () => $"channel/move-user", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); } #endregion #region Channel Permissions - public async Task GetChannelPermissionOverwritesAsync(ulong channelId, RequestOptions options = null) + public async Task GetChannelPermissionOverwritesAsync( + ulong channelId, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: channelId); - return await SendAsync(HttpMethod.Post, () => $"channel-role/index?channel_id={channelId}", ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Post, + () => $"channel-role/index?channel_id={channelId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } public async Task CreateChannelPermissionOverwriteAsync( - CreateOrRemoveChannelPermissionOverwriteParams args, RequestOptions options = null) + CreateOrRemoveChannelPermissionOverwriteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); @@ -632,173 +682,186 @@ public async Task CreateChanne options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: args.ChannelId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel-role/create", args, ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"channel-role/create", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } public async Task ModifyChannelPermissionOverwriteAsync( - ModifyChannelPermissionOverwriteParams args, RequestOptions options = null) + ModifyChannelPermissionOverwriteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); - if (args.TargetType != PermissionOverwriteTargetType.Role) Preconditions.NotEqual(args.TargetId, 0, nameof(args.TargetId)); - + if (args.TargetType != PermissionOverwriteTargetType.Role) + Preconditions.NotEqual(args.TargetId, 0, nameof(args.TargetId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: args.ChannelId); - return await SendJsonAsync(HttpMethod.Post, () => $"channel-role/update", args, ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"channel-role/update", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task RemoveChannelPermissionOverwriteAsync(CreateOrRemoveChannelPermissionOverwriteParams args, RequestOptions options = null) + public async Task SyncChannelPermissionsAsync(SyncChannelPermissionsParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); - Preconditions.NotEqual(args.TargetId, 0, nameof(args.TargetId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: args.ChannelId); - await SendJsonAsync(HttpMethod.Post, () => $"channel-role/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"channel-role/sync", args, ids, clientBucket: ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task SyncChannelPermissionsAsync(SyncChannelPermissionsParams args, RequestOptions options = null) + public async Task RemoveChannelPermissionOverwriteAsync(CreateOrRemoveChannelPermissionOverwriteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); + Preconditions.NotEqual(args.TargetId, 0, nameof(args.TargetId)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: args.ChannelId); - await SendJsonAsync(HttpMethod.Post, () => $"channel-role/sync", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + BucketIds ids = new(channelId: args.ChannelId); + await SendJsonAsync(HttpMethod.Post, + () => $"channel-role/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Messages - public async Task> QueryMessagesAsync(ulong channelId, Guid? referenceMessageId = null, - bool? queryPin = null, Direction dir = Direction.Unspecified, int count = 50, RequestOptions options = null) + public async Task>QueryMessagesAsync(ulong channelId, Guid? referenceMessageId = null, + bool? queryPin = null, Direction dir = Direction.Unspecified, int count = 50, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - if (referenceMessageId is not null) Preconditions.NotEqual(referenceMessageId.Value, Guid.Empty, nameof(referenceMessageId)); - + if (referenceMessageId.HasValue) + Preconditions.NotEqual(referenceMessageId.Value, Guid.Empty, nameof(referenceMessageId)); Preconditions.AtLeast(count, 1, nameof(count)); Preconditions.AtMost(count, 100, nameof(count)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: channelId); string query = $"?target_id={channelId}"; - if (referenceMessageId is not null) query += $"&msg_id={referenceMessageId}"; - - if (queryPin is not null) query += $"&pin={queryPin switch { true => 1, false => 0 }}"; + if (referenceMessageId.HasValue) + query += $"&msg_id={referenceMessageId}"; + if (queryPin.HasValue) + query += $"&pin={queryPin switch { true => 1, false => 0 }}"; string flag = dir switch { Direction.Before => "&flag=before", Direction.Around => "&flag=around", Direction.After => "&flag=after", - Direction.Unspecified => "", + Direction.Unspecified => string.Empty, _ => throw new ArgumentOutOfRangeException(nameof(dir), dir, null) }; query += flag; query += $"&page_size={count}"; QueryMessagesResponse queryMessagesResponse = - await SendAsync(HttpMethod.Get, () => $"message/list{query}", ids, ClientBucketType.SendEdit, options) + await SendAsync(HttpMethod.Get, + () => $"message/list{query}", ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); return queryMessagesResponse.Items; } - public async Task GetMessageAsync(Guid messageId, RequestOptions options = null) + public async Task GetMessageAsync(Guid messageId, RequestOptions? options = null) { Preconditions.NotEqual(messageId, Guid.Empty, nameof(messageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => $"message/view?msg_id={messageId}", ids, ClientBucketType.SendEdit, options) + return await SendAsync(HttpMethod.Get, + () => $"message/view?msg_id={messageId}", ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task CreateMessageAsync(CreateMessageParams args, RequestOptions options = null) + public async Task CreateMessageAsync(CreateMessageParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); - if (args.Content?.Length > KookConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {KookConfig.MaxMessageSize}.", nameof(args.Content)); - options = RequestOptions.CreateOrClone(options); BucketIds ids = new(channelId: args.ChannelId); - return await SendJsonAsync(HttpMethod.Post, () => $"message/create", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"message/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task ModifyMessageAsync(ModifyMessageParams args, RequestOptions options = null) + public async Task ModifyMessageAsync(ModifyMessageParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"message/update", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"message/update", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task DeleteMessageAsync(DeleteMessageParams args, RequestOptions options = null) + public async Task DeleteMessageAsync(DeleteMessageParams args, RequestOptions? options = null) { Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"message/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"message/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task> GetReactionUsersAsync(Guid messageId, string emojiId, RequestOptions options = null) + public async Task>GetReactionUsersAsync(Guid messageId, string emojiId, RequestOptions? options = null) { Preconditions.NotEqual(messageId, Guid.Empty, nameof(messageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); return await SendAsync>(HttpMethod.Get, -#if NET462 - () => $"message/reaction-list?msg_id={messageId}&emoji={WebUtility.UrlEncode(emojiId)}", -#else - () => $"message/reaction-list?msg_id={messageId}&emoji={HttpUtility.UrlEncode(emojiId)}", -#endif - ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + () => $"message/reaction-list?msg_id={messageId}&emoji={UrlEncode(emojiId)}", + ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task AddReactionAsync(AddReactionParams args, RequestOptions options = null) + public async Task AddReactionAsync(AddReactionParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"message/add-reaction", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"message/add-reaction", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task RemoveReactionAsync(RemoveReactionParams args, RequestOptions options = null) + public async Task RemoveReactionAsync(RemoveReactionParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); - if (args.UserId is not null) Preconditions.NotEqual(args.UserId, 0, nameof(args.MessageId)); + if (args.UserId.HasValue) + Preconditions.NotEqual(args.UserId, 0, nameof(args.MessageId)); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"message/delete-reaction", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"message/delete-reaction", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Guild Users - public IAsyncEnumerable> GetAudioChannelsUserConnectsAsync(ulong? guildId = null, - ulong? userId = null, int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, - RequestOptions options = null) + public IAsyncEnumerable> GetAudioChannelsUserConnectsAsync(ulong guildId, ulong userId, + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(guildId ?? 0); + BucketIds ids = new(guildId); return SendPagedAsync(HttpMethod.Get, (pageSize, page) => $"channel-user/get-joined-channel?guild_id={guildId}&user_id={userId}&page_size={pageSize}&page={page}", @@ -809,8 +872,9 @@ public IAsyncEnumerable> GetAudioChannelsUserConnec #region User Chats - public IAsyncEnumerable> GetUserChatsAsync(int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, - RequestOptions options = null) + public IAsyncEnumerable> GetUserChatsAsync( + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, + RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); @@ -820,74 +884,67 @@ public IAsyncEnumerable> GetUserChatsAsync(int lim ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task GetUserChatAsync(Guid chatCode, RequestOptions options = null) + public async Task GetUserChatAsync(Guid chatCode, RequestOptions? options = null) { Preconditions.NotEqual(chatCode, Guid.Empty, nameof(chatCode)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => $"user-chat/view?chat_code={chatCode:N}", ids, ClientBucketType.SendEdit, options) + return await SendAsync(HttpMethod.Get, + () => $"user-chat/view?chat_code={chatCode:N}", ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task CreateUserChatAsync(CreateUserChatParams args, RequestOptions options = null) + public async Task CreateUserChatAsync(CreateUserChatParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); BucketIds ids = new(); - return await SendJsonAsync(HttpMethod.Post, () => $"user-chat/create", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"user-chat/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task DeleteUserChatAsync(DeleteUserChatParams args, RequestOptions options = null) + public async Task DeleteUserChatAsync(DeleteUserChatParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.ChatCode, Guid.Empty, nameof(args.ChatCode)); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"user-chat/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"user-chat/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Direct Messages - public async Task GetDirectMessageAsync(Guid messageId, Guid chatCode, - RequestOptions options = null) + public async Task>QueryDirectMessagesAsync( + Guid? chatCode = null, ulong? userId = null, + Guid? referenceMessageId = null, Direction dir = Direction.Unspecified, + int count = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) { - Preconditions.NotEqual(messageId, Guid.Empty, nameof(messageId)); - Preconditions.NotEqual(chatCode, Guid.Empty, nameof(chatCode)); - options = RequestOptions.CreateOrClone(options); - - BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => $"direct-message/view?msg_id={messageId}&chat_code={chatCode:N}", - ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); - } - - public async Task> QueryDirectMessagesAsync(Guid? chatCode = null, - ulong? userId = null, Guid? referenceMessageId = null, - Direction dir = Direction.Unspecified, int count = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - { - if (chatCode is null && userId is null) + if (!chatCode.HasValue && !userId.HasValue) throw new ArgumentException($"At least one argument must be provided between {nameof(chatCode)} and {nameof(userId)}.", $"{nameof(chatCode)}&{nameof(userId)}"); - - if (referenceMessageId is not null) Preconditions.NotEqual(referenceMessageId.Value, Guid.Empty, nameof(referenceMessageId)); - + if (referenceMessageId.HasValue) + Preconditions.NotEqual(referenceMessageId.Value, Guid.Empty, nameof(referenceMessageId)); Preconditions.AtLeast(count, 1, nameof(count)); Preconditions.AtMost(count, 100, nameof(count)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - string query = (chatCode is not null, userId is not null) switch + string query = (chatCode.HasValue, userId.HasValue) switch { (true, true) => $"?chat_code={chatCode:N}&target_id={userId}", (true, false) => $"?chat_code={chatCode:N}", (false, true) => $"?target_id={userId}", _ => string.Empty }; - if (referenceMessageId is not null) query += $"&msg_id={referenceMessageId}"; + if (referenceMessageId.HasValue) + query += $"&msg_id={referenceMessageId:D}"; string flag = dir switch { @@ -901,15 +958,30 @@ public async Task> QueryDirectMessagesAsync(G query += $"&page_size={count}"; QueryUserChatMessagesResponse queryMessagesResponse = - await SendAsync(HttpMethod.Get, () => $"direct-message/list{query}", ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + await SendAsync(HttpMethod.Get, + () => $"direct-message/list{query}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); return queryMessagesResponse.Items; } - public async Task CreateDirectMessageAsync(CreateDirectMessageParams args, RequestOptions options = null) + public async Task GetDirectMessageAsync(Guid messageId, Guid chatCode, + RequestOptions? options = null) + { + Preconditions.NotEqual(messageId, Guid.Empty, nameof(messageId)); + Preconditions.NotEqual(chatCode, Guid.Empty, nameof(chatCode)); + options = RequestOptions.CreateOrClone(options); + + BucketIds ids = new(); + return await SendAsync(HttpMethod.Get, + () => $"direct-message/view?msg_id={messageId}&chat_code={chatCode:N}", + ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); + } + + public async Task CreateDirectMessageAsync(CreateDirectMessageParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); - if (args.ChatCode is null && args.UserId is null) + if (!args.ChatCode.HasValue && !args.UserId.HasValue) throw new ArgumentException($"At least one argument must be provided between {nameof(args.ChatCode)} and {nameof(args.UserId)}.", $"{nameof(args.ChatCode)}&{nameof(args.UserId)}"); @@ -920,46 +992,48 @@ public async Task CreateDirectMessageAsync(CreateDi options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendJsonAsync(HttpMethod.Post, () => $"direct-message/create", args, ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"direct-message/create", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task ModifyDirectMessageAsync(ModifyDirectMessageParams args, RequestOptions options = null) + public async Task ModifyDirectMessageAsync(ModifyDirectMessageParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"direct-message/update", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"direct-message/update", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task DeleteDirectMessageAsync(DeleteDirectMessageParams args, RequestOptions options = null) + public async Task DeleteDirectMessageAsync(DeleteDirectMessageParams args, RequestOptions? options = null) { Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"direct-message/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"direct-message/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task> GetDirectMessageReactionUsersAsync(Guid messageId, string emojiId, - RequestOptions options = null) + public async Task>GetDirectMessageReactionUsersAsync(Guid messageId, + string emojiId, RequestOptions? options = null) { Preconditions.NotEqual(messageId, Guid.Empty, nameof(messageId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); return await SendAsync>(HttpMethod.Get, -#if NET462 - () => $"direct-message/reaction-list?msg_id={messageId}&emoji={WebUtility.UrlEncode(emojiId)}", -#else - () => $"direct-message/reaction-list?msg_id={messageId}&emoji={HttpUtility.UrlEncode(emojiId)}", -#endif - ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + () => $"direct-message/reaction-list?msg_id={messageId}&emoji={UrlEncode(emojiId)}", + ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task AddDirectMessageReactionAsync(AddReactionParams args, RequestOptions options = null) + public async Task AddDirectMessageReactionAsync(AddReactionParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); @@ -969,7 +1043,7 @@ await SendJsonAsync(HttpMethod.Post, () => $"direct-message/add-reaction", args, .ConfigureAwait(false); } - public async Task RemoveDirectMessageReactionAsync(RemoveReactionParams args, RequestOptions options = null) + public async Task RemoveDirectMessageReactionAsync(RemoveReactionParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.MessageId, Guid.Empty, nameof(args.MessageId)); @@ -984,14 +1058,14 @@ await SendJsonAsync(HttpMethod.Post, () => $"direct-message/delete-reaction", ar #region Gateway - public async Task GetBotGatewayAsync(bool isCompressed = true, RequestOptions options = null) + public async Task GetBotGatewayAsync(bool isCompressed = true, RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); return await SendAsync(HttpMethod.Get, () => $"gateway/index?compress={(isCompressed ? 1 : 0)}", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task GetVoiceGatewayAsync(ulong channelId, RequestOptions options = null) + public async Task GetVoiceGatewayAsync(ulong channelId, RequestOptions? options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); @@ -1003,48 +1077,54 @@ public async Task GetVoiceGatewayAsync(ulong channelId, #region Users - public async Task GetSelfUserAsync(RequestOptions options = null) + public async Task GetSelfUserAsync(RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => "user/me", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Get, + () => "user/me", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task GetUserAsync(ulong userId, RequestOptions options = null) + public async Task GetUserAsync(ulong userId, RequestOptions? options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => $"user/view?user_id={userId}", ids, ClientBucketType.SendEdit, options) + return await SendAsync(HttpMethod.Get, + () => $"user/view?user_id={userId}", ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) + public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions? options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - return await SendAsync(HttpMethod.Get, () => $"user/view?user_id={userId}&guild_id={guildId}", ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Get, + () => $"user/view?user_id={userId}&guild_id={guildId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task GoOfflineAsync(RequestOptions options = null) + public async Task GoOfflineAsync(RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendAsync(HttpMethod.Post, () => $"user/offline", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendAsync(HttpMethod.Post, + () => $"user/offline", ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Friends - public async Task GetFriendStatesAsync(FriendState? friendState, RequestOptions options = null) + public async Task GetFriendStatesAsync(FriendState? friendState, RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); @@ -1057,82 +1137,99 @@ public async Task GetFriendStatesAsync(FriendState? fri null => string.Empty, _ => throw new ArgumentOutOfRangeException(nameof(friendState), friendState, null) }; - return await SendAsync(HttpMethod.Get, () => $"friend{query}", ids, ClientBucketType.SendEdit, options) + return await SendAsync(HttpMethod.Get, + () => $"friend{query}", ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task RequestFriendAsync(RequestFriendParams args, RequestOptions options = null) + public async Task RequestFriendAsync(RequestFriendParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.FullQualification, nameof(args.FullQualification)); if (args.Source is RequestFriendSource.Guild) + { + if (!args.GuildId.HasValue) + throw new ArgumentNullException(nameof(args.GuildId), + "The guild ID must be set when request a friend from a guild."); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); + } options = RequestOptions.CreateOrClone(options); - BucketIds ids = new(args.GuildId); - await SendJsonAsync(HttpMethod.Post, () => "friend/request", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + BucketIds ids = new(args.GuildId ?? 0); + await SendJsonAsync(HttpMethod.Post, + () => "friend/request", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task HandleFriendRequestAsync(HandleFriendRequestParams args, RequestOptions options = null) + public async Task HandleFriendRequestAsync(HandleFriendRequestParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => "friend/handle-request", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => "friend/handle-request", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task RemoveFriendAsync(RemoveFriendParams args, RequestOptions options = null) + public async Task RemoveFriendAsync(RemoveFriendParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => "friend/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => "friend/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task BlockUserAsync(BlockUserParams args, RequestOptions options = null) + public async Task BlockUserAsync(BlockUserParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => "friend/block", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => "friend/block", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task UnblockUserAsync(UnblockUserParams args, RequestOptions options = null) + public async Task UnblockUserAsync(UnblockUserParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => "friend/unblock", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => "friend/unblock", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Assets - public async Task CreateAssetAsync(CreateAssetParams args, RequestOptions options = null) + public async Task CreateAssetAsync(CreateAssetParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendMultipartAsync(HttpMethod.Post, () => $"asset/create", args.ToDictionary(), ids, - ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendMultipartAsync(HttpMethod.Post, + () => $"asset/create", args.ToDictionary(), ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } #endregion #region Guild Roles - public IAsyncEnumerable> GetGuildRolesAsync(ulong guildId, int limit = KookConfig.MaxItemsPerBatchByDefault, - int fromPage = 1, RequestOptions options = null) + public IAsyncEnumerable> GetGuildRolesAsync(ulong guildId, + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -1143,74 +1240,82 @@ public IAsyncEnumerable> GetGuildRolesAsync(ulong guil ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task CreateGuildRoleAsync(CreateGuildRoleParams args, RequestOptions options = null) + public async Task CreateGuildRoleAsync(CreateGuildRoleParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - return await SendJsonAsync(HttpMethod.Post, () => $"guild-role/create", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"guild-role/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task ModifyGuildRoleAsync(ModifyGuildRoleParams args, RequestOptions options = null) + public async Task ModifyGuildRoleAsync(ModifyGuildRoleParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - return await SendJsonAsync(HttpMethod.Post, () => $"guild-role/update", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"guild-role/update", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task DeleteGuildRoleAsync(DeleteGuildRoleParams args, RequestOptions options = null) + public async Task DeleteGuildRoleAsync(DeleteGuildRoleParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - await SendJsonAsync(HttpMethod.Post, () => $"guild-role/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"guild-role/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task AddRoleAsync(AddOrRemoveRoleParams args, RequestOptions options = null) + public async Task AddRoleAsync(AddOrRemoveRoleParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - return await SendJsonAsync(HttpMethod.Post, () => $"guild-role/grant", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"guild-role/grant", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task RemoveRoleAsync(AddOrRemoveRoleParams args, RequestOptions options = null) + public async Task RemoveRoleAsync(AddOrRemoveRoleParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - return await SendJsonAsync(HttpMethod.Post, () => $"guild-role/revoke", args, ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"guild-role/revoke", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } #endregion #region Intimacy - public async Task GetIntimacyAsync(ulong userId, RequestOptions options = null) + public async Task GetIntimacyAsync(ulong userId, RequestOptions? options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendAsync(HttpMethod.Get, () => $"intimacy/index", ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Get, + () => $"intimacy/index", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task UpdateIntimacyValueAsync(UpdateIntimacyValueParams args, RequestOptions options = null) + public async Task UpdateIntimacyValueAsync(UpdateIntimacyValueParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); @@ -1220,15 +1325,17 @@ public async Task UpdateIntimacyValueAsync(UpdateIntimacyValueParams args, Reque options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"intimacy/update", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"intimacy/update", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Guild Emoji - public IAsyncEnumerable> GetGuildEmotesAsync(ulong guildId, int limit = KookConfig.MaxItemsPerBatchByDefault, - int fromPage = 1, RequestOptions options = null) + public IAsyncEnumerable> GetGuildEmotesAsync(ulong guildId, + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -1239,7 +1346,7 @@ public IAsyncEnumerable> GetGuildEmotesAsync(ulong gu ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task CreateGuildEmoteAsync(CreateGuildEmoteParams args, RequestOptions options = null) + public async Task CreateGuildEmoteAsync(CreateGuildEmoteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); if (args.Name is not null) @@ -1247,15 +1354,15 @@ public async Task CreateGuildEmoteAsync(CreateGuildEmoteParams args, Requ Preconditions.AtLeast(args.Name.Length, 2, nameof(args.Name.Length)); Preconditions.AtMost(args.Name.Length, 32, nameof(args.Name.Length)); } - options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - return await SendMultipartAsync(HttpMethod.Post, () => $"guild-emoji/create", args.ToDictionary(), ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + return await SendMultipartAsync(HttpMethod.Post, + () => $"guild-emoji/create", args.ToDictionary(), ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task ModifyGuildEmoteAsync(ModifyGuildEmoteParams args, RequestOptions options = null) + public async Task ModifyGuildEmoteAsync(ModifyGuildEmoteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); if (args.Name is not null) @@ -1263,20 +1370,23 @@ public async Task ModifyGuildEmoteAsync(ModifyGuildEmoteParams args, RequestOpti Preconditions.AtLeast(args.Name.Length, 2, nameof(args.Name.Length)); Preconditions.AtMost(args.Name.Length, 32, nameof(args.Name.Length)); } - options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"guild-emoji/update", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"guild-emoji/update", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task DeleteGuildEmoteAsync(DeleteGuildEmoteParams args, RequestOptions options = null) + public async Task DeleteGuildEmoteAsync(DeleteGuildEmoteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"guild-emoji/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"guild-emoji/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion @@ -1284,20 +1394,19 @@ public async Task DeleteGuildEmoteAsync(DeleteGuildEmoteParams args, RequestOpti #region Guild Invites public IAsyncEnumerable> GetGuildInvitesAsync(ulong? guildId = null, ulong? channelId = null, - int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions options = null) + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { - if (guildId is null && channelId is null) + if (!guildId.HasValue && !channelId.HasValue) throw new ArgumentException($"At least one argument must be provided between {nameof(guildId)} and {nameof(channelId)}.", $"{nameof(guildId)}&{nameof(channelId)}"); - - if (guildId is not null) Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - if (channelId is not null) Preconditions.NotEqual(channelId, 0, nameof(channelId)); - + if (guildId.HasValue) + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + if (channelId.HasValue) + Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId ?? 0, channelId ?? 0); - string query = (guildId is not null, channelId is not null) switch + string query = (guildId.HasValue, channelId.HasValue) switch { (true, true) => $"?guild_id={guildId}&channel_id={channelId}", (true, false) => $"?guild_id={guildId}", @@ -1309,70 +1418,75 @@ public IAsyncEnumerable> GetGuildInvitesAsync(ulong? ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task CreateGuildInviteAsync(CreateGuildInviteParams args, RequestOptions options = null) + public async Task CreateGuildInviteAsync(CreateGuildInviteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); if (args.GuildId is null && args.ChannelId is null) throw new ArgumentException($"At least one argument must be provided between {nameof(args.GuildId)} and {nameof(args.ChannelId)}.", $"{nameof(args.GuildId)}&{nameof(args.ChannelId)}"); - - if (args.GuildId is not null) Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); - - if (args.ChannelId is not null) Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); + if (args.GuildId.HasValue) + Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); + if (args.ChannelId.HasValue) + Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId ?? 0, args.ChannelId ?? 0); - return await SendJsonAsync(HttpMethod.Post, () => $"invite/create", args, ids, ClientBucketType.SendEdit, options) + return await SendJsonAsync(HttpMethod.Post, + () => $"invite/create", args, ids, ClientBucketType.SendEdit, false, options) .ConfigureAwait(false); } - public async Task DeleteGuildInviteAsync(DeleteGuildInviteParams args, RequestOptions options = null) + public async Task DeleteGuildInviteAsync(DeleteGuildInviteParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.UrlCode, nameof(args.UrlCode)); - if (args.GuildId is not null) Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); - - if (args.ChannelId is not null) Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); - + if (args.GuildId.HasValue) + Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); + if (args.ChannelId.HasValue) + Preconditions.NotEqual(args.ChannelId, 0, nameof(args.ChannelId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId ?? 0, args.ChannelId ?? 0); - await SendJsonAsync(HttpMethod.Post, () => $"invite/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"invite/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Guild Bans - public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) + public async Task>GetGuildBansAsync(ulong guildId, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - return await SendAsync>(HttpMethod.Get, () => $"blacklist/list?guild_id={guildId}", ids, ClientBucketType.SendEdit, - options).ConfigureAwait(false); + return await SendAsync>(HttpMethod.Get, + () => $"blacklist/list?guild_id={guildId}", ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task CreateGuildBanAsync(CreateGuildBanParams args, RequestOptions options = null) + public async Task CreateGuildBanAsync(CreateGuildBanParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); Preconditions.NotEqual(args.UserId, 0, nameof(args.UserId)); - if (args.DeleteMessageDays is not null) + if (args.DeleteMessageDays.HasValue) { Preconditions.AtLeast(args.DeleteMessageDays.Value, 0, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); Preconditions.AtMost(args.DeleteMessageDays.Value, 7, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); } - options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - await SendJsonAsync(HttpMethod.Post, () => $"blacklist/create", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"blacklist/create", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task RemoveGuildBanAsync(RemoveGuildBanParams args, RequestOptions options = null) + public async Task RemoveGuildBanAsync(RemoveGuildBanParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.GuildId, 0, nameof(args.GuildId)); @@ -1380,75 +1494,79 @@ public async Task RemoveGuildBanAsync(RemoveGuildBanParams args, RequestOptions options = RequestOptions.CreateOrClone(options); BucketIds ids = new(args.GuildId); - await SendJsonAsync(HttpMethod.Post, () => $"blacklist/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"blacklist/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion #region Badges - public async Task GetGuildBadgeAsync(ulong guildId, BadgeStyle style = BadgeStyle.GuildName, RequestOptions options = null) + public async Task GetGuildBadgeAsync(ulong guildId, BadgeStyle style = BadgeStyle.GuildName, RequestOptions? options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(guildId); - return await SendAsync(HttpMethod.Get, () => $"badge/guild", ids, ClientBucketType.SendEdit, bypassDeserialization: true, - options: options).ConfigureAwait(false); + return await SendAsync(HttpMethod.Get, + () => $"badge/guild?guild_id={guildId}&style={(int) style}", + ids, ClientBucketType.SendEdit, true, options) + .ConfigureAwait(false); } #endregion #region Games - public IAsyncEnumerable> GetGamesAsync(GameCreationSource? source, int limit = KookConfig.MaxItemsPerBatchByDefault, - int fromPage = 1, RequestOptions options = null) + public IAsyncEnumerable> GetGamesAsync(GameCreationSource? source, + int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - int creationSource = source switch - { - GameCreationSource.SelfUser => 1, - GameCreationSource.System => 2, - _ => 0 - }; return SendPagedAsync(HttpMethod.Get, - (pageSize, page) => $"game?type={creationSource}&page_size={pageSize}&page={page}", + (pageSize, page) => $"game?type={(int?) source ?? 0}&page_size={pageSize}&page={page}", ids, ClientBucketType.SendEdit, new PageMeta(fromPage, limit), options); } - public async Task CreateGameAsync(CreateGameParams args, RequestOptions options = null) + public async Task CreateGameAsync(CreateGameParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendJsonAsync(HttpMethod.Post, () => $"game/create", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"game/create", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task ModifyGameAsync(ModifyGameParams args, RequestOptions options = null) + public async Task ModifyGameAsync(ModifyGameParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Id, 0, nameof(args.Id)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - return await SendJsonAsync(HttpMethod.Post, () => $"game/update", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + return await SendJsonAsync(HttpMethod.Post, + () => $"game/update", args, ids, ClientBucketType.SendEdit, false, options) + .ConfigureAwait(false); } - public async Task DeleteGameAsync(DeleteGameParams args, RequestOptions options = null) + public async Task DeleteGameAsync(DeleteGameParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Id, 0, nameof(args.Id)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"game/delete", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"game/delete", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task BeginActivityAsync(BeginActivityParams args, RequestOptions options = null) + public async Task BeginActivityAsync(BeginActivityParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); switch (args.ActivityType) @@ -1459,25 +1577,27 @@ public async Task BeginActivityAsync(BeginActivityParams args, RequestOptions op case ActivityType.Music: if (args.MusicProvider is MusicProvider.Unspecified) throw new ArgumentException($"Value may not be equal to {MusicProvider.Unspecified}", nameof(args.MusicProvider)); - Preconditions.NotNullOrWhitespace(args.Signer, nameof(args.Signer)); Preconditions.NotNullOrWhitespace(args.MusicName, nameof(args.MusicName)); break; } - options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"game/activity", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"game/activity", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } - public async Task EndActivityAsync(EndGameActivityParams args, RequestOptions options = null) + public async Task EndActivityAsync(EndGameActivityParams args, RequestOptions? options = null) { Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); BucketIds ids = new(); - await SendJsonAsync(HttpMethod.Post, () => $"game/delete-activity", args, ids, ClientBucketType.SendEdit, options).ConfigureAwait(false); + await SendJsonAsync(HttpMethod.Post, + () => $"game/delete-activity", args, ids, ClientBucketType.SendEdit, options) + .ConfigureAwait(false); } #endregion @@ -1486,22 +1606,35 @@ public async Task EndActivityAsync(EndGameActivityParams args, RequestOptions op protected void CheckState() { - if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("Client is not logged in."); + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("Client is not logged in."); + } + + [return: NotNullIfNotNull(nameof(content))] + protected static string? UrlEncode(string? content) + { +#if NET462 + return System.Net.WebUtility.UrlEncode(content); +#else + return System.Web.HttpUtility.UrlEncode(content); +#endif } protected static double ToMilliseconds(Stopwatch stopwatch) => - Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + Math.Round((double)stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000.0, 2); - protected string SerializeJson(object payload) => - payload is null - ? string.Empty - : JsonSerializer.Serialize(payload, _serializerOptions); + [return: NotNullIfNotNull(nameof(payload))] + protected string? SerializeJson(object? payload) => + payload is null ? null : JsonSerializer.Serialize(payload, _serializerOptions); protected async Task DeserializeJsonAsync(Stream jsonStream) { try { - return await JsonSerializer.DeserializeAsync(jsonStream, _serializerOptions).ConfigureAwait(false); + T? jsonObject = await JsonSerializer.DeserializeAsync(jsonStream, _serializerOptions).ConfigureAwait(false); + if (jsonObject is null) + throw new JsonException($"Failed to deserialize JSON to type {typeof(T).FullName}"); + return jsonObject; } catch (JsonException ex) { @@ -1519,7 +1652,7 @@ internal class BucketIds { public ulong GuildId { get; internal set; } public ulong ChannelId { get; internal set; } - public HttpMethod HttpMethod { get; internal set; } + public HttpMethod? HttpMethod { get; internal set; } internal BucketIds(ulong guildId = 0, ulong channelId = 0) { @@ -1527,16 +1660,16 @@ internal BucketIds(ulong guildId = 0, ulong channelId = 0) ChannelId = channelId; } - internal object[] ToArray() - => new object[] { HttpMethod, GuildId, ChannelId }; + internal object?[] ToArray() => + [HttpMethod, GuildId, ChannelId]; internal Dictionary ToMajorParametersDictionary() { Dictionary dict = new(); - if (GuildId != 0) dict["GuildId"] = GuildId.ToString(); - - if (ChannelId != 0) dict["ChannelId"] = ChannelId.ToString(); - + if (GuildId != 0) + dict["GuildId"] = GuildId.ToString(); + if (ChannelId != 0) + dict["ChannelId"] = ChannelId.ToString(); return dict; } @@ -1554,15 +1687,17 @@ internal Dictionary ToMajorParametersDictionary() private static string GetEndpoint(Expression> endpointExpr, T1 arg1, T2 arg2) => endpointExpr.Compile()(arg1, arg2); - private static BucketId GetBucketId(HttpMethod httpMethod, BucketIds ids, Expression> endpointExpr, string callingMethod) + private static BucketId GetBucketId(HttpMethod httpMethod, BucketIds ids, Expression> endpointExpr, string? callingMethod) { + Preconditions.NotNull(callingMethod, nameof(callingMethod)); ids.HttpMethod = httpMethod; return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); } private static BucketId GetBucketId(HttpMethod httpMethod, BucketIds ids, Expression> endpointExpr, - TArg1 arg1, TArg2 arg2, string callingMethod) + TArg1 arg1, TArg2 arg2, string? callingMethod) { + Preconditions.NotNull(callingMethod, nameof(callingMethod)); ids.HttpMethod = httpMethod; return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr, arg1, arg2))(ids); } @@ -1576,31 +1711,33 @@ private static Func CreateBucketId(Expression> { //Is this a constant string? if (endpoint.Body.NodeType == ExpressionType.Constant) - return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary()); + return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression)?.Value?.ToString(), x.ToMajorParametersDictionary()); StringBuilder builder = new(); - MethodCallExpression methodCall = endpoint.Body as MethodCallExpression; + + MethodCallExpression methodCall = (MethodCallExpression) endpoint.Body; Expression[] methodArgs = methodCall.Arguments.ToArray(); string format = methodArgs[0].NodeType == ExpressionType.Constant - ? (methodArgs[0] as ConstantExpression).Value as string + ? ((ConstantExpression) methodArgs[0]).Value!.ToString()! : endpoint.Compile()(); //Unpack the array, if one exists (happens with 4+ parameters) if (methodArgs.Length > 1 && methodArgs[1].NodeType == ExpressionType.NewArrayInit) { - NewArrayExpression arrayExpr = methodArgs[1] as NewArrayExpression; + NewArrayExpression arrayExpr = (NewArrayExpression) methodArgs[1]; Expression[] elements = arrayExpr.Expressions.ToArray(); Array.Resize(ref methodArgs, elements.Length + 1); Array.Copy(elements, 0, methodArgs, 1, elements.Length); } int endIndex = format.IndexOf('?'); //Don't include params - if (endIndex == -1) endIndex = format.Length; + if (endIndex == -1) + endIndex = format.Length; int lastIndex = 0; while (true) { - int leftIndex = format.IndexOf("{", lastIndex); + int leftIndex = format.IndexOf("{", lastIndex, StringComparison.Ordinal); if (leftIndex == -1 || leftIndex > endIndex) { builder.Append(format, lastIndex, endIndex - lastIndex); @@ -1608,7 +1745,7 @@ private static Func CreateBucketId(Expression> } builder.Append(format, lastIndex, leftIndex - lastIndex); - int rightIndex = format.IndexOf("}", leftIndex); + int rightIndex = format.IndexOf("}", leftIndex, StringComparison.Ordinal); int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1), NumberStyles.None, CultureInfo.InvariantCulture); string fieldName = GetFieldName(methodArgs[argId + 1]); @@ -1621,12 +1758,14 @@ private static Func CreateBucketId(Expression> && format[rightIndex + 1] == '/') //Ignore the next slash rightIndex++; - if (mappedId.HasValue) builder.Append($"{{{mappedId.Value}}}"); + if (mappedId.HasValue) + builder.Append($"{{{mappedId.Value}}}"); lastIndex = rightIndex + 1; } - if (builder[builder.Length - 1] == '/') builder.Remove(builder.Length - 1, 1); + if (builder[^1] == '/') + builder.Remove(builder.Length - 1, 1); format = builder.ToString(); @@ -1640,11 +1779,13 @@ private static Func CreateBucketId(Expression> private static string GetFieldName(Expression expr) { - if (expr.NodeType == ExpressionType.Convert) expr = (expr as UnaryExpression).Operand; + if (expr.NodeType == ExpressionType.Convert) + expr = ((UnaryExpression) expr).Operand; - if (expr.NodeType != ExpressionType.MemberAccess) throw new InvalidOperationException("Unsupported expression"); + if (expr.NodeType != ExpressionType.MemberAccess) + throw new InvalidOperationException("Unsupported expression"); - return (expr as MemberExpression).Member.Name; + return ((MemberExpression) expr).Member.Name; } #endregion diff --git a/src/Kook.Net.Rest/KookRestClient.cs b/src/Kook.Net.Rest/KookRestClient.cs index ee404748..525135ad 100644 --- a/src/Kook.Net.Rest/KookRestClient.cs +++ b/src/Kook.Net.Rest/KookRestClient.cs @@ -13,15 +13,16 @@ public class KookRestClient : BaseKookClient, IKookClient { #region KookRestClient - internal static readonly JsonSerializerOptions SerializerOptions = new() + private static readonly JsonSerializerOptions SerializerOptions = new() { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; /// /// Gets the logged-in user. /// - public new RestSelfUser CurrentUser + public new RestSelfUser? CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; @@ -30,7 +31,8 @@ public class KookRestClient : BaseKookClient, IKookClient /// /// Initializes a new REST-based KOOK client with the default configuration. /// - public KookRestClient() : this(new KookRestConfig()) + public KookRestClient() + : this(new KookRestConfig()) { } @@ -38,29 +40,31 @@ public KookRestClient() : this(new KookRestConfig()) /// Initializes a new REST-based KOOK client with the specified configuration. /// /// The configuration to use. - public KookRestClient(KookRestConfig config) : base(config, CreateApiClient(config)) + public KookRestClient(KookRestConfig config) + : base(config, CreateApiClient(config)) { } - internal KookRestClient(KookRestConfig config, API.KookRestApiClient api) : base(config, api) + internal KookRestClient(KookRestConfig config, API.KookRestApiClient api) + : base(config, api) { } - private static API.KookRestApiClient CreateApiClient(KookRestConfig config) - => new(config.RestClientProvider, KookConfig.UserAgent, config.AcceptLanguage, + private static API.KookRestApiClient CreateApiClient(KookRestConfig config) => + new(config.RestClientProvider, KookConfig.UserAgent, config.AcceptLanguage, config.DefaultRetryMode, SerializerOptions); internal override void Dispose(bool disposing) { if (disposing) ApiClient.Dispose(); - base.Dispose(disposing); } /// internal override async Task OnLoginAsync(TokenType tokenType, string token) { - SelfUser user = await ApiClient.GetSelfUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + RequestOptions requestOptions = new() { RetryMode = RetryMode.AlwaysRetry }; + SelfUser user = await ApiClient.GetSelfUserAsync(requestOptions).ConfigureAwait(false); ApiClient.CurrentUserId = user.Id; base.CurrentUser = RestSelfUser.Create(this, user); } @@ -68,7 +72,7 @@ internal override async Task OnLoginAsync(TokenType tokenType, string token) internal void CreateRestSelfUser(SelfUser user) => base.CurrentUser = RestSelfUser.Create(this, user); /// - internal override Task OnLogoutAsync() => Task.Delay(0); + internal override Task OnLogoutAsync() => Task.CompletedTask; #endregion @@ -83,8 +87,8 @@ internal override async Task OnLoginAsync(TokenType tokenType, string token) /// A task that represents the asynchronous get operation. The task result contains the guild associated /// with the identifier; null when the guild cannot be found. /// - public Task GetGuildAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetGuildAsync(this, id, options); + public Task GetGuildAsync(ulong id, RequestOptions? options = null) => + ClientHelper.GetGuildAsync(this, id, options); /// /// Gets a collection of guilds that the user is currently in. @@ -94,8 +98,8 @@ public Task GetGuildAsync(ulong id, RequestOptions options = null) /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of guilds that the current user is in. /// - public Task> GetGuildsAsync(RequestOptions options = null) - => ClientHelper.GetGuildsAsync(this, options); + public Task> GetGuildsAsync(RequestOptions? options = null) => + ClientHelper.GetGuildsAsync(this, options); #endregion @@ -110,8 +114,8 @@ public Task> GetGuildsAsync(RequestOptions option /// A task that represents the asynchronous get operation. The task result contains the channel associated /// with the identifier; null when the channel cannot be found. /// - public Task GetChannelAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetChannelAsync(this, id, options); + public Task GetChannelAsync(ulong id, RequestOptions? options = null) => + ClientHelper.GetChannelAsync(this, id, options); /// /// Gets a direct message channel. @@ -122,8 +126,8 @@ public Task GetChannelAsync(ulong id, RequestOptions options = null /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of direct-message channels that the user currently partakes in. /// - public Task GetDMChannelAsync(Guid chatCode, RequestOptions options = null) - => ClientHelper.GetDMChannelAsync(this, chatCode, options); + public Task GetDMChannelAsync(Guid chatCode, RequestOptions? options = null) => + ClientHelper.GetDMChannelAsync(this, chatCode, options); /// /// Gets a collection of direct message channels opened in this session. @@ -140,8 +144,8 @@ public Task GetDMChannelAsync(Guid chatCode, RequestOptions optio /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of direct-message channels that the user currently partakes in. /// - public Task> GetDMChannelsAsync(RequestOptions options = null) - => ClientHelper.GetDMChannelsAsync(this, options); + public Task> GetDMChannelsAsync(RequestOptions? options = null) => + ClientHelper.GetDMChannelsAsync(this, options); #endregion @@ -156,8 +160,8 @@ public Task> GetDMChannelsAsync(RequestOption /// /// A task that represents the asynchronous role addition operation. /// - public Task AddRoleAsync(ulong guildId, ulong userId, uint roleId) - => ClientHelper.AddRoleAsync(this, guildId, userId, roleId); + public Task AddRoleAsync(ulong guildId, ulong userId, uint roleId) => + ClientHelper.AddRoleAsync(this, guildId, userId, roleId); /// /// Removes the specified from this user in the guild. @@ -168,8 +172,8 @@ public Task AddRoleAsync(ulong guildId, ulong userId, uint roleId) /// /// A task that represents the asynchronous role removal operation. /// - public Task RemoveRoleAsync(ulong guildId, ulong userId, uint roleId) - => ClientHelper.RemoveRoleAsync(this, guildId, userId, roleId); + public Task RemoveRoleAsync(ulong guildId, ulong userId, uint roleId) => + ClientHelper.RemoveRoleAsync(this, guildId, userId, roleId); #endregion @@ -184,8 +188,8 @@ public Task RemoveRoleAsync(ulong guildId, ulong userId, uint roleId) /// A task that represents the asynchronous get operation. The task result contains the user associated with /// the identifier; null if the user is not found. /// - public Task GetUserAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetUserAsync(this, id, options); + public Task GetUserAsync(ulong id, RequestOptions? options = null) => + ClientHelper.GetUserAsync(this, id, options); /// /// Gets a user from a guild. @@ -197,8 +201,8 @@ public Task GetUserAsync(ulong id, RequestOptions options = null) /// A task that represents the asynchronous get operation. The task result contains the user from a guild /// associated with the identifier; null if the user is not found in the guild. /// - public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) - => ClientHelper.GetGuildMemberAsync(this, guildId, id, options); + public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions? options = null) => + ClientHelper.GetGuildMemberAsync(this, guildId, id, options); #endregion @@ -212,8 +216,8 @@ public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOpt /// A task that represents the asynchronous get operation. The task result contains a collection of users /// that are friends with the current user. /// - public Task> GetFriendsAsync(RequestOptions options = null) - => ClientHelper.GetFriendsAsync(this, options); + public Task> GetFriendsAsync(RequestOptions? options = null) => + ClientHelper.GetFriendsAsync(this, options); /// /// Gets friend requests. @@ -223,8 +227,8 @@ public Task> GetFriendsAsync(RequestOptions option /// A task that represents the asynchronous get operation. The task result contains a collection of /// friend requests that the current user has received. /// - public Task> GetFriendRequestsAsync(RequestOptions options = null) - => ClientHelper.GetFriendRequestsAsync(this, options); + public Task> GetFriendRequestsAsync(RequestOptions? options = null) => + ClientHelper.GetFriendRequestsAsync(this, options); /// /// Gets blocked users. @@ -234,8 +238,8 @@ public Task> GetFriendRequestsAsync(Reque /// A task that represents the asynchronous get operation. The task result contains a collection of users /// that are blocked by the current user. /// - public Task> GetBlockedUsersAsync(RequestOptions options = null) - => ClientHelper.GetBlockedUsersAsync(this, options); + public Task> GetBlockedUsersAsync(RequestOptions? options = null) => + ClientHelper.GetBlockedUsersAsync(this, options); #endregion @@ -251,8 +255,8 @@ public Task> GetBlockedUsersAsync(RequestOptions o /// A task that represents the asynchronous operation for adding a reaction to the message. /// /// - public Task AddReactionAsync(Guid messageId, IEmote emote, RequestOptions options = null) - => MessageHelper.AddReactionAsync(messageId, emote, this, options); + public Task AddReactionAsync(Guid messageId, IEmote emote, RequestOptions? options = null) => + MessageHelper.AddReactionAsync(messageId, emote, this, options); /// /// Removes a reaction from a message. @@ -265,8 +269,8 @@ public Task AddReactionAsync(Guid messageId, IEmote emote, RequestOptions option /// A task that represents the asynchronous operation for removing a reaction from the message. /// /// - public Task RemoveReactionAsync(Guid messageId, ulong userId, IEmote emote, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(messageId, userId, emote, this, options); + public Task RemoveReactionAsync(Guid messageId, ulong userId, IEmote emote, RequestOptions? options = null) => + MessageHelper.RemoveReactionAsync(messageId, userId, emote, this, options); /// /// Adds a reaction to a direct message. @@ -278,8 +282,8 @@ public Task RemoveReactionAsync(Guid messageId, ulong userId, IEmote emote, Requ /// A task that represents the asynchronous operation for adding a reaction to the direct message. /// /// - public Task AddDirectMessageReactionAsync(Guid messageId, IEmote emote, RequestOptions options = null) - => MessageHelper.AddDirectMessageReactionAsync(messageId, emote, this, options); + public Task AddDirectMessageReactionAsync(Guid messageId, IEmote emote, RequestOptions? options = null) => + MessageHelper.AddDirectMessageReactionAsync(messageId, emote, this, options); /// /// Removes a reaction from a direct message. @@ -292,8 +296,8 @@ public Task AddDirectMessageReactionAsync(Guid messageId, IEmote emote, RequestO /// A task that represents the asynchronous operation for removing a reaction from the direct message. /// /// - public Task RemoveDirectMessageReactionAsync(Guid messageId, ulong userId, IEmote emote, RequestOptions options = null) - => MessageHelper.RemoveDirectMessageReactionAsync(messageId, userId, emote, this, options); + public Task RemoveDirectMessageReactionAsync(Guid messageId, ulong userId, IEmote emote, RequestOptions? options = null) => + MessageHelper.RemoveDirectMessageReactionAsync(messageId, userId, emote, this, options); #endregion @@ -303,21 +307,21 @@ public Task RemoveDirectMessageReactionAsync(Guid messageId, ulong userId, IEmot /// Creates an asset from a file path. /// /// The path to the file. - /// The name of the file. + /// The name of the file. /// The options to be used when sending the request. /// The asset resource URI of the uploaded file. - public Task CreateAssetAsync(string path, string fileName, RequestOptions options = null) - => ClientHelper.CreateAssetAsync(this, File.OpenRead(path), fileName, options); + public Task CreateAssetAsync(string path, string filename, RequestOptions? options = null) => + ClientHelper.CreateAssetAsync(this, File.OpenRead(path), filename, options); /// /// Creates an asset from a stream. /// /// The stream to the file. - /// The name of the file. + /// The name of the file. /// The options to be used when sending the request. /// The asset resource URI of the uploaded file. - public Task CreateAssetAsync(Stream stream, string fileName, RequestOptions options = null) - => ClientHelper.CreateAssetAsync(this, stream, fileName, options); + public Task CreateAssetAsync(Stream stream, string filename, RequestOptions? options = null) => + ClientHelper.CreateAssetAsync(this, stream, filename, options); #endregion @@ -332,8 +336,9 @@ public Task CreateAssetAsync(Stream stream, string fileName, RequestOpti /// /// The options to be used when sending the request. /// A collection of games information. - public IAsyncEnumerable> GetGamesAsync(GameCreationSource? source = null, RequestOptions options = null) - => ClientHelper.GetGamesAsync(this, source, options); + public IAsyncEnumerable> GetGamesAsync( + GameCreationSource? source = null, RequestOptions? options = null) => + ClientHelper.GetGamesAsync(this, source, options); /// /// Creates game information. @@ -343,92 +348,87 @@ public IAsyncEnumerable> GetGamesAsync(GameCreatio /// The icon URI of the game. /// The options to be used when sending the request. /// - public Task CreateGameAsync(string name, string processName, string iconUrl, RequestOptions options = null) - => ClientHelper.CreateGameAsync(this, name, processName, iconUrl, options); + public Task CreateGameAsync(string name, + string? processName = null, string? iconUrl = null, RequestOptions? options = null) => + ClientHelper.CreateGameAsync(this, name, processName, iconUrl, options); #endregion #region IKookClient /// - async Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetGuildAsync(id, options).ConfigureAwait(false); - else - return null; + return null; } /// - async Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + async Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetGuildsAsync(options).ConfigureAwait(false); - else - return ImmutableArray.Create(); + return ImmutableArray.Create(); } /// - async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetUserAsync(id, options).ConfigureAwait(false); - else - return null; + return null; } /// - async Task> IKookClient.GetFriendsAsync(CacheMode mode, RequestOptions options) + async Task> IKookClient.GetFriendsAsync(CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetFriendsAsync(options).ConfigureAwait(false); - else - return null; + return []; } /// - async Task> IKookClient.GetFriendRequestsAsync(CacheMode mode, RequestOptions options) + async Task> IKookClient.GetFriendRequestsAsync(CacheMode mode, + RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetFriendRequestsAsync(options).ConfigureAwait(false); - else - return null; + return []; } /// - async Task> IKookClient.GetBlockedUsersAsync(CacheMode mode, RequestOptions options) + async Task> IKookClient.GetBlockedUsersAsync(CacheMode mode, + RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetBlockedUsersAsync(options).ConfigureAwait(false); - else - return null; + return []; } /// - async Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetChannelAsync(id, options).ConfigureAwait(false); - else - return null; + return null; } /// - async Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions options) + async Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetDMChannelAsync(chatCode, options).ConfigureAwait(false); - else - return null; + return null; } /// - async Task> IKookClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) + async Task> IKookClient.GetDMChannelsAsync(CacheMode mode, + RequestOptions? options) { if (mode == CacheMode.AllowDownload) return await GetDMChannelsAsync(options).ConfigureAwait(false); - else - return ImmutableArray.Create(); + return ImmutableArray.Create(); } #endregion diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ButtonClickEventTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ButtonClickEventTypeConverter.cs index d4df2d81..94832061 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ButtonClickEventTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ButtonClickEventTypeConverter.cs @@ -7,7 +7,7 @@ internal class ButtonClickEventTypeConverter : JsonConverter ButtonClickEventType.None, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ButtonThemeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ButtonThemeConverter.cs index c2e7dc2a..f5128ec4 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ButtonThemeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ButtonThemeConverter.cs @@ -7,7 +7,7 @@ internal class ButtonThemeConverter : JsonConverter { public override ButtonTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string theme = reader.GetString(); + string? theme = reader.GetString(); return theme switch { "primary" => ButtonTheme.Primary, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs index fb3890cf..20e01e7e 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs @@ -7,10 +7,12 @@ namespace Kook.Net.Converters; internal class CardConverter : JsonConverter { - public override CardBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public static readonly CardConverter Instance = new(); + + public override CardBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JsonNode jsonNode = JsonNode.Parse(ref reader); - switch (jsonNode["type"].GetValue()) + JsonNode? jsonNode = JsonNode.Parse(ref reader); + switch (jsonNode?["type"]?.GetValue()) { case "card": return JsonSerializer.Deserialize(jsonNode.ToJsonString(), options); diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardConverterFactory.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverterFactory.cs new file mode 100644 index 00000000..70b665f1 --- /dev/null +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverterFactory.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Kook.API; + +namespace Kook.Net.Converters; + +internal class CardConverterFactory : JsonConverterFactory +{ + public static readonly CardConverterFactory Instance = new(); + + /// + public override bool CanConvert(Type typeToConvert) => + typeToConvert == typeof(CardBase) + || typeToConvert == typeof(ModuleBase) + || typeToConvert == typeof(ElementBase); + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert == typeof(CardBase)) + return CardConverter.Instance; + if (typeToConvert == typeof(ModuleBase)) + return ModuleConverter.Instance; + if (typeToConvert == typeof(ElementBase)) + return ElementConverter.Instance; + return null; + } +} diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardSizeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardSizeConverter.cs index 96601caf..9bb6e073 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CardSizeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardSizeConverter.cs @@ -7,7 +7,7 @@ internal class CardSizeConverter : JsonConverter { public override CardSize Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string size = reader.GetString(); + string? size = reader.GetString(); return size switch { "sm" => CardSize.Small, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardThemeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardThemeConverter.cs index 884e00b4..2574b516 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CardThemeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardThemeConverter.cs @@ -7,7 +7,7 @@ internal class CardThemeConverter : JsonConverter { public override CardTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string theme = reader.GetString(); + string? theme = reader.GetString(); return theme switch { "primary" => CardTheme.Primary, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardTypeConverter.cs index c7777323..3b45914f 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CardTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardTypeConverter.cs @@ -7,7 +7,7 @@ internal class CardTypeConverter : JsonConverter { public override CardType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string type = reader.GetString(); + string? type = reader.GetString(); return type switch { "card" => CardType.Card, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CountdownModeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CountdownModeConverter.cs index 5f61b288..806f1986 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CountdownModeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CountdownModeConverter.cs @@ -7,7 +7,7 @@ internal class CountdownModeConverter : JsonConverter { public override CountdownMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string mode = reader.GetString(); + string? mode = reader.GetString(); return mode switch { "day" => CountdownMode.Day, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs index 658888c8..4e9851c2 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs @@ -7,10 +7,12 @@ namespace Kook.Net.Converters; internal class ElementConverter : JsonConverter { - public override ElementBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public static readonly ElementConverter Instance = new(); + + public override ElementBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JsonNode jsonNode = JsonNode.Parse(ref reader); - return jsonNode["type"].GetValue() switch + JsonNode? jsonNode = JsonNode.Parse(ref reader); + return jsonNode?["type"]?.GetValue() switch { "plain-text" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), "kmarkdown" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ElementTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ElementTypeConverter.cs index a9f669db..a7b05f8e 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ElementTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ElementTypeConverter.cs @@ -7,7 +7,7 @@ internal class ElementTypeConverter : JsonConverter { public override ElementType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string type = reader.GetString(); + string? type = reader.GetString(); return type switch { "plain-text" => ElementType.PlainText, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/HexAlphaColorConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/HexAlphaColorConverter.cs index 0e499f44..4b65f9a8 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/HexAlphaColorConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/HexAlphaColorConverter.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; using System.Text.Json.Serialization; namespace Kook.Net.Converters; @@ -7,11 +8,11 @@ internal class HexAlphaColorConverter : JsonConverter { public override AlphaColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string hex = reader.GetString()?.TrimStart('#'); - if (string.IsNullOrWhiteSpace(hex) || hex.Length < 8) + string? hex = reader.GetString()?.TrimStart('#'); + if (hex == null || string.IsNullOrWhiteSpace(hex) || hex.Length < 8) return AlphaColor.Default; - return new AlphaColor(uint.Parse(hex, System.Globalization.NumberStyles.HexNumber)); + return new AlphaColor(uint.Parse(hex, NumberStyles.HexNumber)); } public override void Write(Utf8JsonWriter writer, AlphaColor value, JsonSerializerOptions options) => diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/HexColorConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/HexColorConverter.cs index 109924c9..995575dd 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/HexColorConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/HexColorConverter.cs @@ -7,8 +7,8 @@ internal class HexColorConverter : JsonConverter { public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string hex = reader.GetString()?.TrimStart('#'); - if (string.IsNullOrWhiteSpace(hex) || hex.Length < 6) + string? hex = reader.GetString()?.TrimStart('#'); + if (hex == null || string.IsNullOrWhiteSpace(hex) || hex.Length < 6) return Color.Default; return new Color(uint.Parse(hex, System.Globalization.NumberStyles.HexNumber)); diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ImageSizeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ImageSizeConverter.cs index c3fb04a0..62b68713 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ImageSizeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ImageSizeConverter.cs @@ -7,7 +7,7 @@ internal class ImageSizeConverter : JsonConverter { public override ImageSize Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string size = reader.GetString(); + string? size = reader.GetString(); return size switch { "sm" => ImageSize.Small, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs index 37a1c61c..8355edaa 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs @@ -7,10 +7,12 @@ namespace Kook.Net.Converters; internal class ModuleConverter : JsonConverter { - public override ModuleBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public static readonly ModuleConverter Instance = new(); + + public override ModuleBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JsonNode jsonNode = JsonNode.Parse(ref reader); - return jsonNode["type"].GetValue() switch + JsonNode? jsonNode = JsonNode.Parse(ref reader); + return jsonNode?["type"]?.GetValue() switch { "header" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), "section" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleTypeConverter.cs index 0c99801e..9bf51541 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleTypeConverter.cs @@ -7,7 +7,7 @@ internal class ModuleTypeConverter : JsonConverter { public override ModuleType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string type = reader.GetString(); + string? type = reader.GetString(); return type switch { "header" => ModuleType.Header, diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/RawValueColorConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/RawValueColorConverter.cs index 46b5bbcc..b682de7b 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/RawValueColorConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/RawValueColorConverter.cs @@ -11,5 +11,6 @@ public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe return new Color(rawValue); } - public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) => writer.WriteNumberValue(value.RawValue); + public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) => + writer.WriteNumberValue(value.RawValue); } diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/SectionAccessoryModeConvertercs.cs b/src/Kook.Net.Rest/Net/Converters/Cards/SectionAccessoryModeConvertercs.cs index bdb59b7d..970c2fbe 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/SectionAccessoryModeConvertercs.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/SectionAccessoryModeConvertercs.cs @@ -3,25 +3,25 @@ namespace Kook.Net.Converters; -internal class SectionAccessoryModeConverter : JsonConverter +internal class SectionAccessoryModeConverter : JsonConverter { - public override SectionAccessoryMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SectionAccessoryMode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string mode = reader.GetString(); + string? mode = reader.GetString(); return mode switch { "left" => SectionAccessoryMode.Left, "right" => SectionAccessoryMode.Right, - _ => SectionAccessoryMode.Unspecified + _ => null }; } - public override void Write(Utf8JsonWriter writer, SectionAccessoryMode value, JsonSerializerOptions options) => + public override void Write(Utf8JsonWriter writer, SectionAccessoryMode? value, JsonSerializerOptions options) => writer.WriteStringValue(value switch { SectionAccessoryMode.Left => "left", SectionAccessoryMode.Right => "right", - SectionAccessoryMode.Unspecified => null, + null => null, _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) }); } diff --git a/src/Kook.Net.Rest/Net/Converters/ChatCodeConverter.cs b/src/Kook.Net.Rest/Net/Converters/ChatCodeConverter.cs index bffbceb2..84ec6ae9 100644 --- a/src/Kook.Net.Rest/Net/Converters/ChatCodeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/ChatCodeConverter.cs @@ -6,7 +6,8 @@ namespace Kook.Net.Converters; internal class ChatCodeConverter : JsonConverter { public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - Guid.Parse(reader.GetString() ?? throw new InvalidCastException("Chat code Guid parse error")); + Guid.Parse(reader.GetString() ?? throw new InvalidCastException("Cannot convert null to Guid")); - public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString("N")); + public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) => + writer.WriteStringValue(value.ToString("N")); } diff --git a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs index f281f197..9e119e11 100644 --- a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs @@ -7,16 +7,19 @@ namespace Kook.Net.Converters; internal class EmbedConverter : JsonConverter { - public override EmbedBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override EmbedBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JsonNode jsonNode = JsonNode.Parse(ref reader); - string rawType = jsonNode["type"].GetValue(); + JsonNode? jsonNode = JsonNode.Parse(ref reader); + if (jsonNode == null) return null; + string? rawType = jsonNode["type"]?.GetValue(); + if (rawType == null) return null; return rawType switch { "link" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), "image" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), "bili-video" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - _ => new API.NotImplementedEmbed(rawType, jsonNode["url"].GetValue(), jsonNode) + "card" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), + _ => new API.NotImplementedEmbed(rawType, jsonNode) }; } @@ -33,6 +36,9 @@ public override void Write(Utf8JsonWriter writer, EmbedBase value, JsonSerialize case EmbedType.BilibiliVideo: writer.WriteRawValue(JsonSerializer.Serialize(value as API.BilibiliVideoEmbed, options)); break; + case EmbedType.Card: + writer.WriteRawValue(JsonSerializer.Serialize(value as API.CardEmbed, options)); + break; default: writer.WriteRawValue((value as API.NotImplementedEmbed)!.RawJsonNode.ToString()); break; diff --git a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedTypeConverter.cs index 05baed55..6d86c791 100644 --- a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedTypeConverter.cs @@ -7,12 +7,13 @@ internal class EmbedTypeConverter : JsonConverter { public override EmbedType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string type = reader.GetString(); + string? type = reader.GetString(); return type switch { "link" => EmbedType.Link, "image" => EmbedType.Image, "bili-video" => EmbedType.BilibiliVideo, + "card" => EmbedType.Card, _ => EmbedType.NotImplemented }; } @@ -23,6 +24,7 @@ public override void Write(Utf8JsonWriter writer, EmbedType value, JsonSerialize EmbedType.Link => "link", EmbedType.Image => "image", EmbedType.BilibiliVideo => "bili-video", + EmbedType.Card => "card", _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) }); } diff --git a/src/Kook.Net.Rest/Net/Converters/FriendStateConverter.cs b/src/Kook.Net.Rest/Net/Converters/FriendStateConverter.cs index 82e80c6a..890d3ba6 100644 --- a/src/Kook.Net.Rest/Net/Converters/FriendStateConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/FriendStateConverter.cs @@ -7,7 +7,7 @@ internal class FriendStateConverter : JsonConverter { public override FriendState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string provider = reader.GetString(); + string? provider = reader.GetString(); return provider switch { "request" => FriendState.Pending, diff --git a/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs b/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs index da0ccc0e..b4573703 100644 --- a/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs @@ -7,12 +7,12 @@ internal class GuildFeaturesConverter : JsonConverter { public override GuildFeatures Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - IList rawValues = JsonSerializer.Deserialize>(ref reader, options); + IList rawValues = JsonSerializer.Deserialize>(ref reader, options) ?? []; GuildFeature features = GuildFeature.None; foreach (string item in rawValues) { - if (Enum.TryParse(item, true, out var result)) + if (Enum.TryParse(item, true, out GuildFeature result)) features |= result; } @@ -26,7 +26,7 @@ public override void Write(Utf8JsonWriter writer, GuildFeatures value, JsonSeria writer.WriteStartArray(); foreach (object enumValue in enumValues) { - var val = (GuildFeature)enumValue; + GuildFeature val = (GuildFeature)enumValue; if (val is GuildFeature.None) continue; diff --git a/src/Kook.Net.Rest/Net/Converters/MusicProviderConverter.cs b/src/Kook.Net.Rest/Net/Converters/MusicProviderConverter.cs index ed0d4fd6..6fe2ca73 100644 --- a/src/Kook.Net.Rest/Net/Converters/MusicProviderConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/MusicProviderConverter.cs @@ -7,7 +7,7 @@ internal class MusicProviderConverter : JsonConverter { public override MusicProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string provider = reader.GetString(); + string? provider = reader.GetString(); return provider switch { "cloudmusic" => MusicProvider.NetEaseCloudMusic, diff --git a/src/Kook.Net.Rest/Net/Converters/NullableChatCodeConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableChatCodeConverter.cs index cbf2325f..a6fe3024 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableChatCodeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableChatCodeConverter.cs @@ -5,10 +5,17 @@ namespace Kook.Net.Converters; internal class NullableChatCodeConverter : JsonConverter { - public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - Guid.TryParse(reader.GetString() ?? string.Empty, out Guid guid) + /// + public override bool HandleNull => true; + + public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + return Guid.TryParse(reader.GetString(), out Guid guid) ? guid : null; + } public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) { diff --git a/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetConverter.cs deleted file mode 100644 index b93b5285..00000000 --- a/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Kook.Net.Converters; - -internal class NullableDateTimeOffsetConverter : JsonConverter -{ - public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - long tick = reader.GetInt64(); - if (tick == 0) return null; - - return DateTimeOffset.FromUnixTimeMilliseconds(tick); - } - - public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) - { - if (value is null) - writer.WriteNumberValue(0); - else - writer.WriteNumberValue(value.Value.ToUnixTimeMilliseconds()); - } -} diff --git a/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetUnixTimeMillisecondsConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetUnixTimeMillisecondsConverter.cs new file mode 100644 index 00000000..fa135e23 --- /dev/null +++ b/src/Kook.Net.Rest/Net/Converters/NullableDateTimeOffsetUnixTimeMillisecondsConverter.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kook.Net.Converters; + +internal class NullableDateTimeOffsetUnixTimeMillisecondsConverter : JsonConverter +{ + /// + public override bool HandleNull => true; + + public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + long tick = reader.GetInt64(); + if (tick == 0) + return null; + + return DateTimeOffset.FromUnixTimeMilliseconds(tick); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) + { +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + if (!value.HasValue || value == DateTimeOffset.MinValue || value == DateTimeOffset.UnixEpoch) +#else + if (!value.HasValue || value == DateTimeOffset.MinValue || value == new DateTimeOffset(621355968000000000L, TimeSpan.Zero)) +#endif + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.Value.ToUnixTimeMilliseconds()); + } +} diff --git a/src/Kook.Net.Rest/Net/Converters/NullableGradientColorConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableGradientColorConverter.cs index daf217e6..140d606e 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableGradientColorConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableGradientColorConverter.cs @@ -5,16 +5,22 @@ namespace Kook.Net.Converters; internal class NullableGradientColorConverter : JsonConverter { + /// + public override bool HandleNull => true; + /// public override GradientColor? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + return null; + if (reader.TokenType == JsonTokenType.StartObject) { reader.Read(); if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException($"{nameof(NullableGradientColorConverter)} expects property name token, but got {reader.TokenType}"); - string propertyName = reader.GetString(); + string? propertyName = reader.GetString(); if (propertyName != "color_list") throw new JsonException($"{nameof(NullableGradientColorConverter)} expects property name 'color_list', but got {propertyName}"); diff --git a/src/Kook.Net.Rest/Net/Converters/NullableGuidConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableGuidConverter.cs index cbfc7151..c7564b3f 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableGuidConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableGuidConverter.cs @@ -5,10 +5,17 @@ namespace Kook.Net.Converters; internal class NullableGuidConverter : JsonConverter { - public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - Guid.TryParse(reader.GetString() ?? string.Empty, out Guid guid) + /// + public override bool HandleNull => true; + + public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + return Guid.TryParse(reader.GetString(), out Guid guid) ? guid : Guid.Empty; + } public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) { diff --git a/src/Kook.Net.Rest/Net/Converters/NullableTimeSpanConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableTimeSpanConverter.cs index 78947acc..b4cdd005 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableTimeSpanConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableTimeSpanConverter.cs @@ -5,10 +5,16 @@ namespace Kook.Net.Converters; internal class NullableTimeSpanConverter : JsonConverter { + /// + public override bool HandleNull => true; + public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + return null; ulong tick = reader.GetUInt64(); - if (tick == 0) return null; + if (tick == 0) + return null; return TimeSpan.FromSeconds(tick); } diff --git a/src/Kook.Net.Rest/Net/Converters/NullableUInt32Converter.cs b/src/Kook.Net.Rest/Net/Converters/NullableUInt32Converter.cs index 0a969db9..5df68c76 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableUInt32Converter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableUInt32Converter.cs @@ -5,12 +5,17 @@ namespace Kook.Net.Converters; internal class NullableUInt32Converter : JsonConverter { + /// + public override bool HandleNull => true; + public override uint? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { + case JsonTokenType.Null: + return null; case JsonTokenType.String: - string value = reader.GetString(); + string? value = reader.GetString(); return !string.IsNullOrWhiteSpace(value) && uint.TryParse(value, out uint result) ? result : null; diff --git a/src/Kook.Net.Rest/Net/Converters/NullableUInt64Converter.cs b/src/Kook.Net.Rest/Net/Converters/NullableUInt64Converter.cs index d0d24ffb..2d9a8c0d 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableUInt64Converter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableUInt64Converter.cs @@ -5,9 +5,14 @@ namespace Kook.Net.Converters; internal class NullableUInt64Converter : JsonConverter { + /// + public override bool HandleNull => true; + public override ulong? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string value = reader.GetString(); + if (reader.TokenType == JsonTokenType.Null) + return null; + string? value = reader.GetString(); return !string.IsNullOrWhiteSpace(value) && ulong.TryParse(value, out ulong result) ? result : null; diff --git a/src/Kook.Net.Rest/Net/Converters/NullableVoiceQualityConverter.cs b/src/Kook.Net.Rest/Net/Converters/NullableVoiceQualityConverter.cs index e2c2f1e9..792d5225 100644 --- a/src/Kook.Net.Rest/Net/Converters/NullableVoiceQualityConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/NullableVoiceQualityConverter.cs @@ -5,12 +5,15 @@ namespace Kook.Net.Converters; internal class NullableVoiceQualityConverter : JsonConverter { + /// + public override bool HandleNull => true; + public override VoiceQuality? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { case JsonTokenType.String: - string str = reader.GetString(); + string? str = reader.GetString(); return int.TryParse(str, out int result) ? (VoiceQuality?)result : null; diff --git a/src/Kook.Net.Rest/Net/Converters/PageSortInfoConverter.cs b/src/Kook.Net.Rest/Net/Converters/PageSortInfoConverter.cs index 1d43eccb..9c342f96 100644 --- a/src/Kook.Net.Rest/Net/Converters/PageSortInfoConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/PageSortInfoConverter.cs @@ -33,16 +33,13 @@ public override PageSortInfo Read(ref Utf8JsonReader reader, Type typeToConvert, public override void Write(Utf8JsonWriter writer, PageSortInfo value, JsonSerializerOptions options) { writer.WriteStartObject(); - switch (value.SortMode) + if (value.SortKey is not null) { - case API.Rest.SortMode.Ascending: + if (value.SortMode == API.Rest.SortMode.Ascending) writer.WriteNumber(value.SortKey, 1); - break; - case API.Rest.SortMode.Descending: + else if (value.SortMode == API.Rest.SortMode.Descending) writer.WriteNumber(value.SortKey, -1); - break; } - writer.WriteEndObject(); } } diff --git a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs index 83481bbb..e8d38c4c 100644 --- a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs @@ -7,14 +7,16 @@ namespace Kook.Net.Converters; internal class PokeResourceConverter : JsonConverter { - public override PokeResourceBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PokeResourceBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JsonNode jsonNode = JsonNode.Parse(ref reader); - string rawType = jsonNode["type"].GetValue(); + JsonNode? jsonNode = JsonNode.Parse(ref reader); + if (jsonNode == null) return null; + string? rawType = jsonNode["type"]?.GetValue(); + if (rawType == null) return null; return rawType switch { "ImageAnimation" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - _ => new API.NotImplementedPokeResource(rawType, jsonNode) + _ => new API.NotImplementedPokeResource(rawType, jsonNode) { Type = PokeResourceType.NotImplemented } }; } diff --git a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceTypeConverter.cs index 9a951c55..6bd0a640 100644 --- a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceTypeConverter.cs @@ -7,7 +7,7 @@ internal class PokeResourceTypeConverter : JsonConverter { public override PokeResourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string type = reader.GetString(); + string? type = reader.GetString(); return type switch { "ImageAnimation" => PokeResourceType.ImageAnimation, diff --git a/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs b/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs index 2499daf4..62f21957 100644 --- a/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs @@ -6,7 +6,7 @@ namespace Kook.Net.Converters; internal class QuoteConverter : JsonConverter { /// - public override API.Quote Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + public override API.Quote? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch { JsonTokenType.Null or JsonTokenType.String => null, diff --git a/src/Kook.Net.Rest/Net/Converters/SafeAttachmentConverter.cs b/src/Kook.Net.Rest/Net/Converters/SafeAttachmentConverter.cs new file mode 100644 index 00000000..8a7a3bf8 --- /dev/null +++ b/src/Kook.Net.Rest/Net/Converters/SafeAttachmentConverter.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kook.Net.Converters; + +internal class SafeAttachmentConverter : JsonConverter +{ + /// + public override API.Attachment? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.StartArray) + { + reader.Skip(); + return null; + } + + API.Attachment attachment = new() + { + Type = string.Empty, + Url = string.Empty, + }; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + return attachment; + + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException(); + + string? propertyName = reader.GetString(); + reader.Read(); + switch (propertyName) + { + case "type": + attachment.Type = reader.GetString() + ?? throw new JsonException("Required property missing: type"); + break; + case "url": + attachment.Url = reader.GetString() + ?? throw new JsonException("Required property missing: url"); + break; + case "name": + attachment.Name = reader.GetString(); + break; + case "file_type": + attachment.FileType = reader.GetString(); + break; + case "size": + attachment.Size = reader.GetInt32(); + break; + case "duration": + attachment.Duration = reader.GetDouble(); + break; + case "width": + attachment.Width = reader.GetInt32(); + break; + case "height": + attachment.Height = reader.GetInt32(); + break; + default: + reader.Skip(); + break; + } + } + + throw new JsonException($"Unexpected end when reading {nameof(API.Attachment)}."); + } + + /// + public override void Write(Utf8JsonWriter writer, API.Attachment? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteString("type", value.Type); + writer.WriteString("url", value.Url); + writer.WriteString("name", value.Name); + writer.WriteString("file_type", value.FileType); + if (value.Size.HasValue) + writer.WriteNumber("size", value.Size.Value); + if (value.Duration.HasValue) + writer.WriteNumber("duration", value.Duration.Value); + if (value.Width.HasValue) + writer.WriteNumber("width", value.Width.Value); + if (value.Height.HasValue) + writer.WriteNumber("height", value.Height.Value); + writer.WriteEndObject(); + } +} diff --git a/src/Kook.Net.Rest/Net/Converters/SafeUInt64Converter.cs b/src/Kook.Net.Rest/Net/Converters/SafeUInt64Converter.cs new file mode 100644 index 00000000..4ce827b0 --- /dev/null +++ b/src/Kook.Net.Rest/Net/Converters/SafeUInt64Converter.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kook.Net.Converters; + +internal class SafeUInt64Converter : JsonConverter +{ + /// + public override bool HandleNull => true; + + public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return 0; + string? value = reader.GetString(); + return !string.IsNullOrWhiteSpace(value) && ulong.TryParse(value, out ulong result) + ? result + : 0; + } + + public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/Kook.Net.Rest/Net/DefaultRestClient.cs b/src/Kook.Net.Rest/Net/DefaultRestClient.cs index 97de5f0d..8a0994aa 100644 --- a/src/Kook.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Kook.Net.Rest/Net/DefaultRestClient.cs @@ -70,7 +70,7 @@ private void Dispose(bool disposing) public void Dispose() => Dispose(true); - public void SetHeader(string key, string value) + public void SetHeader(string key, string? value) { _client.DefaultRequestHeaders.Remove(key); if (value != null) _client.DefaultRequestHeaders.Add(key, value); @@ -78,8 +78,9 @@ public void SetHeader(string key, string value) public void SetCancellationToken(CancellationToken cancellationToken) => _cancellationToken = cancellationToken; - public async Task SendAsync(HttpMethod method, string endpoint, CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null) + public async Task SendAsync(HttpMethod method, string endpoint, CancellationToken cancellationToken, + string? reason = null, + IEnumerable>>? requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (HttpRequestMessage restRequest = new(method, uri)) @@ -94,8 +95,9 @@ public async Task SendAsync(HttpMethod method, string endpoint, Ca } } - public async Task SendAsync(HttpMethod method, string endpoint, string json, CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null) + public async Task SendAsync(HttpMethod method, string endpoint, string json, + CancellationToken cancellationToken, string? reason = null, + IEnumerable>>? requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using HttpRequestMessage restRequest = new(method, uri); @@ -104,18 +106,15 @@ public async Task SendAsync(HttpMethod method, string endpoint, st if (requestHeaders != null) foreach (KeyValuePair> header in requestHeaders) restRequest.Headers.Add(header.Key, header.Value); -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - restRequest.Content = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); -#else restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); -#endif return await SendInternalAsync(restRequest, cancellationToken).ConfigureAwait(false); } /// Unsupported param type. - public async Task SendAsync(HttpMethod method, string endpoint, IReadOnlyDictionary multipartParams, - CancellationToken cancellationToken, string reason = null, - IEnumerable>> requestHeaders = null) + public async Task SendAsync(HttpMethod method, string endpoint, + IReadOnlyDictionary multipartParams, + CancellationToken cancellationToken, string? reason = null, + IEnumerable>>? requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); @@ -145,16 +144,13 @@ static StreamContent GetStreamContent(Stream stream) stream.Position = 0; } -#pragma warning disable IDISP004 return new StreamContent(stream); -#pragma warning restore IDISP004 } - foreach (var p in multipartParams ?? ImmutableDictionary.Empty) + foreach (KeyValuePair p in multipartParams ?? ImmutableDictionary.Empty) { switch (p.Value) { -#pragma warning disable IDISP004 case string stringValue: { content.Add(new StringContent(stringValue, Encoding.UTF8, "text/plain"), p.Key); continue; } case byte[] byteArrayValue: @@ -163,14 +159,13 @@ static StreamContent GetStreamContent(Stream stream) { content.Add(GetStreamContent(streamValue), p.Key); continue; } case MultipartFile fileValue: { - var streamContent = GetStreamContent(fileValue.Stream); - + StreamContent streamContent = GetStreamContent(fileValue.Stream); if (fileValue.ContentType != null) streamContent.Headers.ContentType = new MediaTypeHeaderValue(fileValue.ContentType); - - content.Add(streamContent, p.Key, fileValue.Filename); -#pragma warning restore IDISP004 - + if (fileValue.Filename is not null) + content.Add(streamContent, p.Key, fileValue.Filename); + else + content.Add(streamContent, p.Key); continue; } default: @@ -190,27 +185,24 @@ private async Task SendInternalAsync(HttpRequestMessage request, C using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, cancellationToken); #if DEBUG_REST Debug.WriteLine($"[REST] [{id}] {request.Method} {request.RequestUri} {request.Content?.Headers.ContentType?.MediaType}"); -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (request.Content?.Headers.ContentType?.MediaType == MediaTypeNames.Application.Json) -#else if (request.Content?.Headers.ContentType?.MediaType == "application/json") -#endif Debug.WriteLine($"[REST] {await request.Content.ReadAsStringAsync().ConfigureAwait(false)}"); #endif cancellationToken = cancellationTokenSource.Token; HttpResponseMessage response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); - Dictionary headers = - response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + Dictionary headers = []; + foreach (KeyValuePair> kvp in response.Headers) + { + string? value = kvp.Value.FirstOrDefault(); + headers[kvp.Key] = value; + } + // ReSharper disable once MethodSupportsCancellation Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); #if DEBUG_REST Debug.WriteLine($"[REST] [{id}] {response.StatusCode} {response.ReasonPhrase}"); -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (response.Content?.Headers.ContentType?.MediaType == MediaTypeNames.Application.Json) -#else if (response.Content?.Headers.ContentType?.MediaType == "application/json") -#endif Debug.WriteLine($"[REST] [{id}] {await response.Content.ReadAsStringAsync().ConfigureAwait(false)}"); #endif return new RestResponse(response.StatusCode, headers, stream, response.Content?.Headers.ContentType); diff --git a/src/Kook.Net.Rest/Net/Queue/ClientBucket.cs b/src/Kook.Net.Rest/Net/Queue/ClientBucket.cs index 0a80062b..196a88ba 100644 --- a/src/Kook.Net.Rest/Net/Queue/ClientBucket.cs +++ b/src/Kook.Net.Rest/Net/Queue/ClientBucket.cs @@ -15,21 +15,22 @@ internal struct ClientBucket static ClientBucket() { - ClientBucket[] buckets = new[] - { + ClientBucket[] buckets = + [ new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "", null), 10, 10), new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "", null), 10, 10) - }; - - ImmutableDictionary.Builder builder = ImmutableDictionary.CreateBuilder(); - foreach (ClientBucket bucket in buckets) builder.Add(bucket.Type, bucket); - - DefsByType = builder.ToImmutable(); - - ImmutableDictionary.Builder builder2 = ImmutableDictionary.CreateBuilder(); - foreach (ClientBucket bucket in buckets) builder2.Add(bucket.Id, bucket); - - DefsById = builder2.ToImmutable(); + ]; + + ImmutableDictionary.Builder typeBuilder = + ImmutableDictionary.CreateBuilder(); + foreach (ClientBucket bucket in buckets) + typeBuilder.Add(bucket.Type, bucket); + DefsByType = typeBuilder.ToImmutable(); + + ImmutableDictionary.Builder idBuilder = + ImmutableDictionary.CreateBuilder(); + foreach (ClientBucket bucket in buckets) idBuilder.Add(bucket.Id, bucket); + DefsById = idBuilder.ToImmutable(); } public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; diff --git a/src/Kook.Net.Rest/Net/Queue/GatewayBucket.cs b/src/Kook.Net.Rest/Net/Queue/GatewayBucket.cs index ea79e34c..5ec1ace0 100644 --- a/src/Kook.Net.Rest/Net/Queue/GatewayBucket.cs +++ b/src/Kook.Net.Rest/Net/Queue/GatewayBucket.cs @@ -14,21 +14,23 @@ internal struct GatewayBucket static GatewayBucket() { - GatewayBucket[] buckets = new[] - { + GatewayBucket[] buckets = + [ // Limit is 120/60s, but 3 will be reserved for heartbeats (2 for possible heartbeats in the same timeframe and a possible failure) new GatewayBucket(GatewayBucketType.Unbucketed, BucketId.Create(null, "", null), 117, 60) - }; - - ImmutableDictionary.Builder builder = ImmutableDictionary.CreateBuilder(); - foreach (GatewayBucket bucket in buckets) builder.Add(bucket.Type, bucket); - - DefsByType = builder.ToImmutable(); - - ImmutableDictionary.Builder builder2 = ImmutableDictionary.CreateBuilder(); - foreach (GatewayBucket bucket in buckets) builder2.Add(bucket.Id, bucket); - - DefsById = builder2.ToImmutable(); + ]; + + ImmutableDictionary.Builder typeBuilder = + ImmutableDictionary.CreateBuilder(); + foreach (GatewayBucket bucket in buckets) + typeBuilder.Add(bucket.Type, bucket); + DefsByType = typeBuilder.ToImmutable(); + + ImmutableDictionary.Builder idBuilder = + ImmutableDictionary.CreateBuilder(); + foreach (GatewayBucket bucket in buckets) + idBuilder.Add(bucket.Id, bucket); + DefsById = idBuilder.ToImmutable(); } public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; diff --git a/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs b/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs index 2b8987fe..84165e79 100644 --- a/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs @@ -9,14 +9,14 @@ namespace Kook.Net.Queue; internal class RequestQueue : IDisposable, IAsyncDisposable { - public event Func RateLimitTriggered; + public event Func? RateLimitTriggered; private readonly ConcurrentDictionary _buckets; private readonly SemaphoreSlim _tokenLock; private readonly CancellationTokenSource _cancellationTokenSource; //Dispose token private CancellationTokenSource _clearToken; private CancellationToken _parentToken; - private CancellationTokenSource _requestCancellationTokenSource; + private CancellationTokenSource? _requestCancellationTokenSource; private CancellationToken _requestCancellationToken; //Parent token + Clear token private DateTimeOffset _waitUntil; @@ -38,7 +38,7 @@ public RequestQueue() public async Task SetCancellationTokenAsync(CancellationToken cancellationToken) { - await _tokenLock.WaitAsync().ConfigureAwait(false); + await _tokenLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { _parentToken = cancellationToken; @@ -54,7 +54,7 @@ public async Task SetCancellationTokenAsync(CancellationToken cancellationToken) public async Task ClearAsync() { - await _tokenLock.WaitAsync().ConfigureAwait(false); + await _tokenLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { _clearToken?.Cancel(); @@ -72,7 +72,7 @@ public async Task ClearAsync() public async Task SendAsync(RestRequest request) { - CancellationTokenSource createdTokenSource = null; + CancellationTokenSource? createdTokenSource = null; if (request.Options.CancellationToken.CanBeCanceled) { createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancellationToken, request.Options.CancellationToken); @@ -89,7 +89,7 @@ public async Task SendAsync(RestRequest request) public async Task SendAsync(WebSocketRequest request) { - CancellationTokenSource createdTokenSource = null; + CancellationTokenSource? createdTokenSource = null; if (request.Options.CancellationToken.CanBeCanceled) { createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancellationToken, request.Options.CancellationToken); @@ -109,9 +109,9 @@ internal async Task EnterGlobalAsync(int id, RestRequest request) if (millis > 0) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive) [Global]"); + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive) [Global]"); #endif - await Task.Delay(millis).ConfigureAwait(false); + await Task.Delay(millis, CancellationToken.None).ConfigureAwait(false); } } @@ -121,21 +121,25 @@ internal void PauseGlobal(RateLimitInfo info) => internal async Task EnterGlobalAsync(int id, WebSocketRequest request) { //If this is a global request (unbucketed), it'll be dealt in EnterAsync - GatewayBucket requestBucket = GatewayBucket.Get(request.Options.BucketId); + BucketId? bucketId = request.Options.BucketId; + if (bucketId is null) return; + GatewayBucket requestBucket = GatewayBucket.Get(bucketId); if (requestBucket.Type == GatewayBucketType.Unbucketed) return; //It's not a global request, so need to remove one from global (per-session) GatewayBucket globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed); RequestOptions options = RequestOptions.CreateOrClone(request.Options); options.BucketId = globalBucketType.Id; - WebSocketRequest globalRequest = new(null, null, false, false, options); + WebSocketRequest globalRequest = new(null, [], false, false, options); RequestBucket globalBucket = GetOrCreateBucket(options, globalRequest); await globalBucket.TriggerAsync(id, globalRequest); } private RequestBucket GetOrCreateBucket(RequestOptions options, IRequest request) { - BucketId bucketId = options.BucketId; + BucketId? bucketId = options.BucketId; + if (bucketId is null) + throw new InvalidOperationException("BucketId is not set."); object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x)); if (obj is BucketId hashBucket) { @@ -146,10 +150,13 @@ private RequestBucket GetOrCreateBucket(RequestOptions options, IRequest request return (RequestBucket)obj; } - internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info, string endpoint) => - await RateLimitTriggered(bucketId, info, endpoint).ConfigureAwait(false); + internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info, string? endpoint) + { + if (RateLimitTriggered is null) return; + await RateLimitTriggered.Invoke(bucketId, info, endpoint).ConfigureAwait(false); + } - internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string kookHash) + internal (RequestBucket?, BucketId?) UpdateBucketHash(BucketId id, string kookHash) { if (!id.IsHashBucket) { @@ -180,8 +187,13 @@ private async Task RunCleanup() if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) { if (bucket.Id.IsHashBucket) - foreach (BucketId redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value)) + { + IEnumerable redirectBuckets = _buckets + .Where(x => x.Value == bucket.Id) + .Select(x => (BucketId)x.Value); + foreach (BucketId redirectBucket in redirectBuckets) _buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket + } _buckets.TryRemove(bucket.Id, out _); } @@ -216,7 +228,7 @@ public void Dispose() public async ValueTask DisposeAsync() { - if (!(_cancellationTokenSource is null)) + if (_cancellationTokenSource is not null) { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); @@ -227,4 +239,4 @@ public async ValueTask DisposeAsync() _clearToken?.Dispose(); _requestCancellationTokenSource?.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Kook.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Kook.Net.Rest/Net/Queue/RequestQueueBucket.cs index 7a12d932..5245e5e2 100644 --- a/src/Kook.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Kook.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -19,8 +19,8 @@ internal class RequestBucket private readonly RequestQueue _queue; private int _semaphore; private DateTimeOffset? _resetTick; - private RequestBucket _redirectBucket; - private JsonSerializerOptions _serializerOptions; + private RequestBucket? _redirectBucket; + private readonly JsonSerializerOptions _serializerOptions; public BucketId Id { get; private set; } public int WindowCount { get; private set; } @@ -30,16 +30,17 @@ public RequestBucket(RequestQueue queue, IRequest request, BucketId id) { _serializerOptions = new JsonSerializerOptions { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; _queue = queue; Id = id; _lock = new object(); - if (request.Options.IsClientBucket) + if (request.Options.IsClientBucket && request.Options.BucketId != null) WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; - else if (request.Options.IsGatewayBucket) + else if (request.Options.IsGatewayBucket && request.Options.BucketId != null) WindowCount = GatewayBucket.Get(request.Options.BucketId).WindowCount; else WindowCount = 1; //Only allow one request until we get a header back @@ -55,17 +56,18 @@ public async Task SendAsync(RestRequest request) { int id = Interlocked.Increment(ref nextId); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Start"); + Debug.WriteLine($"[{id}] Start"); #endif LastAttemptAt = DateTimeOffset.UtcNow; while (true) { await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false); - if (_redirectBucket != null) return await _redirectBucket.SendAsync(request); + if (_redirectBucket != null) + return await _redirectBucket.SendAsync(request); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sending..."); + Debug.WriteLine($"[{id}] Sending..."); #endif RestResponse response = default; RateLimitInfo info = default; @@ -83,14 +85,14 @@ public async Task SendAsync(RestRequest request) if (info.IsGlobal) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] (!) 429 [Global]"); + Debug.WriteLine($"[{id}] (!) 429 [Global]"); #endif _queue.PauseGlobal(info); } else { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] (!) 429"); + Debug.WriteLine($"[{id}] (!) 429"); #endif } @@ -98,14 +100,14 @@ public async Task SendAsync(RestRequest request) continue; //Retry case HttpStatusCode.BadGateway: //502 #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] (!) 502"); + Debug.WriteLine($"[{id}] (!) 502"); #endif if ((request.Options.RetryMode & RetryMode.Retry502) == 0) throw new HttpException(HttpStatusCode.BadGateway, request, null); continue; //Retry default: - API.Rest.RestResponseBase responseBase = null; + API.Rest.RestResponseBase? responseBase = null; if (response.Stream != null) try { @@ -123,47 +125,45 @@ public async Task SendAsync(RestRequest request) responseBase?.Code, responseBase?.Message, responseBase?.Data is not null - ? new KookJsonError[] - { + ? + [ new("root", - new KookError[] { new(((int)responseBase.Code).ToString(), responseBase.Message) } + [new(((int)responseBase.Code).ToString(), responseBase.Message)] ) - } + ] : null ); } else { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Success"); -#endif -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (response.MediaTypeHeader.MediaType == MediaTypeNames.Application.Json) -#else - if (response.MediaTypeHeader.MediaType == "application/json") + Debug.WriteLine($"[{id}] Success"); #endif + if (response.MediaTypeHeader?.MediaType == "application/json") { - API.Rest.RestResponseBase responseBase = + API.Rest.RestResponseBase? responseBase = await JsonSerializer.DeserializeAsync(response.Stream, _serializerOptions); if (responseBase?.Code > (KookErrorCode)0) + { throw new HttpException( response.StatusCode, request, responseBase.Code, responseBase.Message, responseBase.Data is not null - ? new KookJsonError[] - { - new("root", - new KookError[] { new(((int)responseBase.Code).ToString(), responseBase.Message) } + ? + [ + new KookJsonError("root", + [new KookError(((int)responseBase.Code).ToString(), responseBase.Message)] ) - } + ] : null ); + } return new MemoryStream(Encoding.UTF8.GetBytes(responseBase?.Data.ToString() ?? string.Empty)); } - else if (response.MediaTypeHeader.MediaType == "image/svg+xml") + else if (response.MediaTypeHeader?.MediaType == "image/svg+xml") return response.Stream; } } @@ -171,9 +171,10 @@ responseBase.Data is not null catch (TimeoutException) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Timeout"); + Debug.WriteLine($"[{id}] Timeout"); #endif - if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) throw; + if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) + throw; await Task.Delay(500).ConfigureAwait(false); continue; //Retry @@ -193,7 +194,7 @@ responseBase.Data is not null { UpdateRateLimit(id, request, info, response.StatusCode == (HttpStatusCode)429); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Stop"); + Debug.WriteLine($"[{id}] Stop"); #endif } } @@ -203,7 +204,7 @@ public async Task SendAsync(WebSocketRequest request) { int id = Interlocked.Increment(ref nextId); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Start"); + Debug.WriteLine($"[{id}] Start"); #endif LastAttemptAt = DateTimeOffset.UtcNow; while (true) @@ -212,7 +213,7 @@ public async Task SendAsync(WebSocketRequest request) await EnterAsync(id, request).ConfigureAwait(false); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sending..."); + Debug.WriteLine($"[{id}] Sending..."); #endif try { @@ -222,9 +223,10 @@ public async Task SendAsync(WebSocketRequest request) catch (TimeoutException) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Timeout"); + Debug.WriteLine($"[{id}] Timeout"); #endif - if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) throw; + if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) + throw; await Task.Delay(500).ConfigureAwait(false); continue; //Retry @@ -242,9 +244,9 @@ public async Task SendAsync(WebSocketRequest request) }*/ finally { - UpdateRateLimit(id, request, default(RateLimitInfo), false); + UpdateRateLimit(id, request, default, false); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Stop"); + Debug.WriteLine($"[{id}] Stop"); #endif } } @@ -256,7 +258,7 @@ internal async Task TriggerAsync(int id, IRequest request) Debug.WriteLine($"[{id}] Trigger Bucket"); #endif await EnterAsync(id, request).ConfigureAwait(false); - UpdateRateLimit(id, request, default(RateLimitInfo), false); + UpdateRateLimit(id, request, default, false); } private async Task EnterAsync(int id, IRequest request) @@ -273,8 +275,7 @@ private async Task EnterAsync(int id, IRequest request) { if (!isRateLimited) throw new TimeoutException(); - else - ThrowRetryLimit(request); + ThrowRetryLimit(request); } lock (_lock) @@ -312,7 +313,7 @@ private async Task EnterAsync(int id, IRequest request) if (ignoreRatelimit) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Ignoring ratelimit"); + Debug.WriteLine($"[{id}] Ignoring ratelimit"); #endif break; } @@ -322,19 +323,21 @@ private async Task EnterAsync(int id, IRequest request) if (resetAt.HasValue && resetAt > DateTimeOffset.UtcNow) { - if (resetAt > timeoutAt) ThrowRetryLimit(request); + if (resetAt > timeoutAt) + ThrowRetryLimit(request); int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); #endif if (millis > 0) await Task.Delay(millis, request.Options.CancellationToken).ConfigureAwait(false); } else { - if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < MinimumSleepTimeMs) ThrowRetryLimit(request); + if ((timeoutAt - DateTimeOffset.UtcNow)?.TotalMilliseconds < MinimumSleepTimeMs) + ThrowRetryLimit(request); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)"); + Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)"); #endif await Task.Delay(MinimumSleepTimeMs, request.Options.CancellationToken).ConfigureAwait(false); } @@ -351,19 +354,20 @@ private async Task EnterAsync(int id, IRequest request) private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429, bool redirected = false) { - if (WindowCount == 0) return; + if (WindowCount == 0) + return; lock (_lock) { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Raw RateLimitInto: IsGlobal: {info.IsGlobal}, Limit: {info.Limit}, Remaining: {info.Remaining}, ResetAfter: {info.ResetAfter?.TotalSeconds}"); + Debug.WriteLine($"[{id}] Raw RateLimitInto: IsGlobal: {info.IsGlobal}, Limit: {info.Limit}, Remaining: {info.Remaining}, ResetAfter: {info.ResetAfter?.TotalSeconds}"); #endif if (redirected) { // we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same Interlocked.Decrement(ref _semaphore); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Decrease Semaphore"); + Debug.WriteLine($"[{id}] Decrease Semaphore"); #endif } @@ -371,14 +375,14 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool if (info.Bucket != null && !redirected) { - (RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket); - if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null)) + (RequestBucket?, BucketId?) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket); + if (hashBucket.Item1 is not null && hashBucket.Item2 is not null) { if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue { Id = hashBucket.Item2; #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})"); + Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})"); #endif } else @@ -388,7 +392,7 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool // update the hash bucket ratelimit _redirectBucket.UpdateRateLimit(id, request, info, is429, true); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}"); + Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}"); #endif return; } @@ -399,7 +403,7 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool { WindowCount = info.Limit.Value; #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Updated Limit to {WindowCount}"); + Debug.WriteLine($"[{id}] Updated Limit to {WindowCount}"); #endif } @@ -407,7 +411,7 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool { _semaphore = info.Remaining.Value; #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Updated Semaphore (Remaining) to {_semaphore}"); + Debug.WriteLine($"[{id}] Updated Semaphore (Remaining) to {_semaphore}"); #endif } @@ -479,7 +483,7 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif - Task _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); + _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); } return; @@ -497,24 +501,26 @@ private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool if (!hasQueuedReset || resetTick > _resetTick) { _resetTick = resetTick; - LastAttemptAt = resetTick.Value; //Make sure we don't destroy this until after its been reset + LastAttemptAt = resetTick.Value; //Make sure we don't destroy this until after it's been reset #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); + Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif if (!hasQueuedReset) - { - Task _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); - } + _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); } } } private async Task QueueReset(int id, int millis, IRequest request) { + if (_resetTick == null) + return; + while (true) { - if (millis > 0) await Task.Delay(millis).ConfigureAwait(false); + if (millis > 0) + await Task.Delay(millis).ConfigureAwait(false); lock (_lock) { @@ -522,7 +528,7 @@ private async Task QueueReset(int id, int millis, IRequest request) if (millis <= 0) //Make sure we haven't gotten a more accurate reset time { #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] * Reset *"); + Debug.WriteLine($"[{id}] * Reset *"); #endif _semaphore = WindowCount; _resetTick = null; @@ -534,6 +540,7 @@ private async Task QueueReset(int id, int millis, IRequest request) private void ThrowRetryLimit(IRequest request) { - if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) throw new RateLimitedException(request); + if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) + throw new RateLimitedException(request); } -} \ No newline at end of file +} diff --git a/src/Kook.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs b/src/Kook.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs index 06a1d610..db8e87e4 100644 --- a/src/Kook.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs +++ b/src/Kook.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs @@ -10,10 +10,13 @@ internal class JsonRestRequest : RestRequest { public string Json { get; } - public JsonRestRequest(IRestClient client, HttpMethod method, string endpoint, string json, RequestOptions options) - : base(client, method, endpoint, options) => + public JsonRestRequest(IRestClient client, HttpMethod method, string endpoint, string json, RequestOptions? options) + : base(client, method, endpoint, options) + { Json = json; + } public override async Task SendAsync() => await Client - .SendAsync(Method, Endpoint, Json, Options.CancellationToken, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false); + .SendAsync(Method, Endpoint, Json, Options.CancellationToken, Options.AuditLogReason, Options.RequestHeaders) + .ConfigureAwait(false); } diff --git a/src/Kook.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs b/src/Kook.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs index 26c0ea7c..cb7bd7ec 100644 --- a/src/Kook.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs +++ b/src/Kook.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs @@ -10,11 +10,15 @@ internal class MultipartRestRequest : RestRequest { public IReadOnlyDictionary MultipartParams { get; } - public MultipartRestRequest(IRestClient client, HttpMethod method, string endpoint, IReadOnlyDictionary multipartParams, - RequestOptions options) - : base(client, method, endpoint, options) => + public MultipartRestRequest(IRestClient client, HttpMethod method, string endpoint, + IReadOnlyDictionary multipartParams, RequestOptions? options) + : base(client, method, endpoint, options) + { MultipartParams = multipartParams; + } public override async Task SendAsync() => await Client - .SendAsync(Method, Endpoint, MultipartParams, Options.CancellationToken, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false); + .SendAsync(Method, Endpoint, MultipartParams, Options.CancellationToken, + Options.AuditLogReason, Options.RequestHeaders) + .ConfigureAwait(false); } diff --git a/src/Kook.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Kook.Net.Rest/Net/Queue/Requests/RestRequest.cs index 91df2983..eb5640c4 100644 --- a/src/Kook.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Kook.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -15,7 +15,7 @@ internal class RestRequest : IRequest public TaskCompletionSource Promise { get; } public RequestOptions Options { get; } - public RestRequest(IRestClient client, HttpMethod method, string endpoint, RequestOptions options) + public RestRequest(IRestClient client, HttpMethod method, string endpoint, RequestOptions? options) { Preconditions.NotNull(options, nameof(options)); @@ -28,5 +28,6 @@ public RestRequest(IRestClient client, HttpMethod method, string endpoint, Reque } public virtual async Task SendAsync() => - await Client.SendAsync(Method, Endpoint, Options.CancellationToken, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false); + await Client.SendAsync(Method, Endpoint, Options.CancellationToken, Options.AuditLogReason, Options.RequestHeaders) + .ConfigureAwait(false); } diff --git a/src/Kook.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs b/src/Kook.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs index f5ad9808..506d23cc 100644 --- a/src/Kook.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs +++ b/src/Kook.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs @@ -4,7 +4,7 @@ namespace Kook.Net.Queue; internal class WebSocketRequest : IRequest { - public IWebSocketClient Client { get; } + public IWebSocketClient? Client { get; } public byte[] Data { get; } public bool IsText { get; } public bool IgnoreLimit { get; } @@ -12,7 +12,7 @@ internal class WebSocketRequest : IRequest public TaskCompletionSource Promise { get; } public RequestOptions Options { get; } - public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, bool ignoreLimit, RequestOptions options) + public WebSocketRequest(IWebSocketClient? client, byte[] data, bool isText, bool ignoreLimit, RequestOptions? options) { Preconditions.NotNull(options, nameof(options)); @@ -25,5 +25,13 @@ public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, bool Promise = new TaskCompletionSource(); } - public async Task SendAsync() => await Client.SendAsync(Data, 0, Data.Length, IsText).ConfigureAwait(false); + public async Task SendAsync() + { + if (Client == null) + { + Promise.SetException(new InvalidOperationException("WebSocket client is not set.")); + return; + } + await Client.SendAsync(Data, 0, Data.Length, IsText).ConfigureAwait(false); + } } diff --git a/src/Kook.Net.Rest/Net/RateLimitInfo.cs b/src/Kook.Net.Rest/Net/RateLimitInfo.cs index e3505b42..b1c8a05f 100644 --- a/src/Kook.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Kook.Net.Rest/Net/RateLimitInfo.cs @@ -26,7 +26,7 @@ public struct RateLimitInfo : IRateLimitInfo public TimeSpan? ResetAfter { get; } /// - public string Bucket { get; } + public string? Bucket { get; } /// public TimeSpan? Lag { get; } @@ -34,31 +34,31 @@ public struct RateLimitInfo : IRateLimitInfo /// public string Endpoint { get; } - internal RateLimitInfo(Dictionary headers, string endpoint) + internal RateLimitInfo(Dictionary headers, string endpoint) { Endpoint = endpoint; IsGlobal = headers.ContainsKey("X-Rate-Limit-Global"); - Limit = headers.TryGetValue("X-Rate-Limit-Limit", out string temp) + Limit = headers.TryGetValue("X-Rate-Limit-Limit", out string? temp) && int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out int limit) ? limit - : (int?)null; + : null; Remaining = headers.TryGetValue("X-Rate-Limit-Remaining", out temp) && int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out int remaining) ? remaining - : (int?)null; + : null; // RetryAfter = headers.TryGetValue("Retry-After", out temp) && // int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; ResetAfter = headers.TryGetValue("X-Rate-Limit-Reset", out temp) && double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double resetAfter) ? TimeSpan.FromSeconds(resetAfter) - : (TimeSpan?)null; + : null; // Reset = headers.TryGetValue("X-Rate-Limit-Reset", out temp) && // double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) && reset != 0 ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; Bucket = headers.TryGetValue("X-Rate-Limit-Bucket", out temp) ? temp : null; Lag = headers.TryGetValue("Date", out temp) && DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset date) ? DateTimeOffset.UtcNow - date - : (TimeSpan?)null; + : null; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/DirectMessageButtonClickEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/DirectMessageButtonClickEvent.cs new file mode 100644 index 00000000..0722c4c7 --- /dev/null +++ b/src/Kook.Net.WebSocket/API/Gateway/DirectMessageButtonClickEvent.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Kook.API.Gateway; + +internal class DirectMessageButtonClickEvent +{ + [JsonPropertyName("value")] + public required string Value { get; set; } + + [JsonPropertyName("msg_id")] + public Guid MessageId { get; set; } + + [JsonPropertyName("user_id")] + public ulong UserId { get; set; } + + [JsonPropertyName("target_id")] + public ulong ChannelId { get; set; } + + [JsonPropertyName("channel_type")] + public required string ChannelType { get; set; } + + [JsonPropertyName("user_info")] + public required User User { get; set; } + + [JsonPropertyName("guild_id")] + public ulong? GuildId { get; set; } +} diff --git a/src/Kook.Net.WebSocket/API/Gateway/DirectMessageUpdateEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/DirectMessageUpdateEvent.cs index d11c677e..4eb50b34 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/DirectMessageUpdateEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/DirectMessageUpdateEvent.cs @@ -16,12 +16,23 @@ internal class DirectMessageUpdateEvent public Guid MessageId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("updated_at")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset UpdatedAt { get; set; } + [JsonPropertyName("kmarkdown")] + public MentionInfo? MentionInfo { get; set; } + + [JsonPropertyName("quote")] + [JsonConverter(typeof(QuoteConverter))] + public Quote? Quote { get; set; } + + [JsonPropertyName("attachments")] + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } + [JsonPropertyName("chat_code")] [JsonConverter(typeof(ChatCodeConverter))] public Guid ChatCode { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewayEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewayEvent.cs index 8423c211..55a5876b 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewayEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewayEvent.cs @@ -1,25 +1,29 @@ +using System.Text.Json; using Kook.Net.Converters; using System.Text.Json.Serialization; namespace Kook.API.Gateway; -internal class GatewayEvent +internal class GatewayEvent { [JsonPropertyName("channel_type")] - public string ChannelType { get; set; } + public required string ChannelType { get; set; } [JsonPropertyName("type")] - [JsonConverter(typeof(MessageTypeConverter))] public MessageType Type { get; set; } [JsonPropertyName("target_id")] + [JsonConverter(typeof(SafeUInt64Converter))] public ulong TargetId { get; set; } [JsonPropertyName("author_id")] public uint AuthorId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } + + [JsonPropertyName("extra")] + public required T ExtraData { get; set; } [JsonPropertyName("msg_id")] public Guid MessageId { get; set; } @@ -27,10 +31,4 @@ internal class GatewayEvent [JsonPropertyName("msg_timestamp")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset MessageTimestamp { get; set; } - - [JsonPropertyName("nonce")] - public string Nonce { get; set; } - - [JsonPropertyName("extra")] - public object ExtraData { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewayGroupMessageExtraData.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewayGroupMessageExtraData.cs index 83ee5f8c..d784b688 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewayGroupMessageExtraData.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewayGroupMessageExtraData.cs @@ -6,40 +6,40 @@ namespace Kook.API.Gateway; internal class GatewayGroupMessageExtraData { [JsonPropertyName("type")] - [JsonConverter(typeof(MessageTypeConverter))] public MessageType Type { get; set; } [JsonPropertyName("guild_id")] public ulong GuildId { get; set; } [JsonPropertyName("channel_name")] - public string ChannelName { get; set; } + public string? ChannelName { get; set; } + + [JsonPropertyName("author")] + public required Rest.GuildMember Author { get; set; } [JsonPropertyName("mention")] - public ulong[] MentionedUsers { get; set; } + public ulong[]? MentionedUsers { get; set; } [JsonPropertyName("mention_all")] public bool MentionedAll { get; set; } [JsonPropertyName("mention_roles")] - public uint[] MentionedRoles { get; set; } + public uint[]? MentionedRoles { get; set; } [JsonPropertyName("mention_here")] public bool MentionedHere { get; set; } [JsonPropertyName("nav_channels")] - public ulong[] MentionedChannels { get; set; } + public ulong[]? MentionedChannels { get; set; } - [JsonPropertyName("author")] - public Rest.GuildMember Author { get; set; } + [JsonPropertyName("kmarkdown")] + public KMarkdownInfo? KMarkdownInfo { get; set; } [JsonPropertyName("quote")] [JsonConverter(typeof(QuoteConverter))] - public Quote Quote { get; set; } + public Quote? Quote { get; set; } [JsonPropertyName("attachments")] - public Attachment Attachment { get; set; } - - [JsonPropertyName("kmarkdown")] - public KMarkdownInfo KMarkdownInfo { get; set; } + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewayHelloPayload.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewayHelloPayload.cs index d75210fd..14770d0f 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewayHelloPayload.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewayHelloPayload.cs @@ -8,6 +8,5 @@ internal class GatewayHelloPayload public KookErrorCode Code { get; set; } [JsonPropertyName("session_id")] - // [JsonConverter(typeof(GuidConverter))] public Guid SessionId { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewayPersonMessageExtraData.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewayPersonMessageExtraData.cs index 5220e3fb..76b9c89e 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewayPersonMessageExtraData.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewayPersonMessageExtraData.cs @@ -6,7 +6,6 @@ namespace Kook.API.Gateway; internal class GatewayPersonMessageExtraData { [JsonPropertyName("type")] - [JsonConverter(typeof(MessageTypeConverter))] public MessageType Type { get; set; } [JsonPropertyName("code")] @@ -14,21 +13,19 @@ internal class GatewayPersonMessageExtraData public Guid Code { get; set; } [JsonPropertyName("author")] - public User Author { get; set; } + public required User Author { get; set; } - [JsonPropertyName("nonce")] - public string Nonce { get; set; } - - [JsonPropertyName("last_msg_content")] - public string LastMessageContent { get; set; } + [JsonPropertyName("mention")] + public ulong[]? MentionedUsers { get; set; } [JsonPropertyName("quote")] [JsonConverter(typeof(QuoteConverter))] - public Quote Quote { get; set; } + public Quote? Quote { get; set; } [JsonPropertyName("attachments")] - public Attachment Attachment { get; set; } + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } [JsonPropertyName("kmarkdown")] - public KMarkdownInfo KMarkdownInfo { get; set; } + public required KMarkdownInfo KMarkdownInfo { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewayReconnectPayload.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewayReconnectPayload.cs index fd9e878f..fc808859 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewayReconnectPayload.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewayReconnectPayload.cs @@ -8,5 +8,5 @@ internal class GatewayReconnectPayload public KookErrorCode Code { get; set; } [JsonPropertyName("err")] - public string Message { get; set; } + public string? Message { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrame.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrame.cs index 2e608620..2108aa79 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrame.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrame.cs @@ -6,7 +6,6 @@ namespace Kook.API.Gateway; internal class GatewaySocketFrame { [JsonPropertyName("s")] - [JsonConverter(typeof(GatewaySocketFrameTypeConverter))] public GatewaySocketFrameType Type { get; set; } [JsonPropertyName("sn")] @@ -14,5 +13,5 @@ internal class GatewaySocketFrame [JsonPropertyName("d")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object Payload { get; set; } + public object? Payload { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrameType.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrameType.cs index 89f3c3ae..302f1408 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrameType.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewaySocketFrameType.cs @@ -2,11 +2,11 @@ namespace Kook.API.Gateway; internal enum GatewaySocketFrameType { - Event, - Hello, - Ping, - Pong, - Resume, - Reconnect, - ResumeAck + Event = 0, + Hello = 1, + Ping = 2, + Pong = 3, + Resume = 4, + Reconnect = 5, + ResumeAck = 6 } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GatewaySystemEventExtraData.cs b/src/Kook.Net.WebSocket/API/Gateway/GatewaySystemEventExtraData.cs index 3be51668..dd28deea 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GatewaySystemEventExtraData.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GatewaySystemEventExtraData.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace Kook.API.Gateway; @@ -5,8 +6,8 @@ namespace Kook.API.Gateway; internal class GatewaySystemEventExtraData { [JsonPropertyName("type")] - public string Type { get; set; } + public required string Type { get; set; } [JsonPropertyName("body")] - public object Body { get; set; } + public JsonElement Body { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildBanEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildBanEvent.cs index 7a99849f..d64f8ecd 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildBanEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GuildBanEvent.cs @@ -8,8 +8,8 @@ internal class GuildBanEvent public ulong OperatorUserId { get; set; } [JsonPropertyName("remark")] - public string Reason { get; set; } + public string? Reason { get; set; } [JsonPropertyName("user_id")] - public ulong[] UserIds { get; set; } + public required ulong[] UserIds { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildEmojiEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildEmojiEvent.cs index 86e64363..f9baa373 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildEmojiEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GuildEmojiEvent.cs @@ -5,10 +5,10 @@ namespace Kook.API.Gateway; internal class GuildEmojiEvent { [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("id")] - public string Id { get; set; } + public required string Id { get; set; } [JsonPropertyName("emoji_type")] public EmojiType Type { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildEvent.cs index 8a9c4cd6..20890ab2 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GuildEvent.cs @@ -9,19 +9,19 @@ internal class GuildEvent public ulong GuildId { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public required string Name { get; set; } [JsonPropertyName("user_id")] - public ulong UserId { get; set; } + public ulong UserId { get; set; } // TODO: 服务器拥有者更新 [JsonPropertyName("icon")] - public string Icon { get; set; } + public required string Icon { get; set; } [JsonPropertyName("notify_type")] public NotifyType NotifyType { get; set; } [JsonPropertyName("region")] - public string Region { get; set; } + public required string Region { get; set; } [JsonPropertyName("enable_open")] [JsonConverter(typeof(NumberBooleanConverter))] @@ -38,13 +38,13 @@ internal class GuildEvent public ulong WelcomeChannelId { get; set; } [JsonPropertyName("banner")] - public string Banner { get; set; } + public required string Banner { get; set; } // TODO: 服务器横幅更新 [JsonPropertyName("banner_status")] public int BannerStatus { get; set; } [JsonPropertyName("custom_id")] - public string CustomId { get; set; } + public required string CustomId { get; set; } [JsonPropertyName("boost_num")] public int BoostSubscriptionCount { get; set; } @@ -57,4 +57,7 @@ internal class GuildEvent [JsonPropertyName("status")] public int Status { get; set; } + + [JsonPropertyName("auto_delete_time")] + public string? AutoDeleteTime { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineEvent.cs deleted file mode 100644 index a36a5a8b..00000000 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Kook.Net.Converters; -using System.Text.Json.Serialization; - -namespace Kook.API.Gateway; - -internal class GuildMemberOnlineEvent -{ - [JsonPropertyName("user_id")] - public ulong UserId { get; set; } - - [JsonPropertyName("event_time")] - [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset OnlineAt { get; set; } - - [JsonPropertyName("guilds")] - public ulong[] CommonGuilds { get; set; } -} diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOfflineEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineOfflineEvent.cs similarity index 67% rename from src/Kook.Net.WebSocket/API/Gateway/GuildMemberOfflineEvent.cs rename to src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineOfflineEvent.cs index 3068cd5b..5e83f5f9 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOfflineEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GuildMemberOnlineOfflineEvent.cs @@ -3,15 +3,15 @@ namespace Kook.API.Gateway; -internal class GuildMemberOfflineEvent +internal class GuildMemberOnlineOfflineEvent { [JsonPropertyName("user_id")] public ulong UserId { get; set; } [JsonPropertyName("event_time")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] - public DateTimeOffset OfflineAt { get; set; } + public DateTimeOffset EventTime { get; set; } [JsonPropertyName("guilds")] - public ulong[] CommonGuilds { get; set; } + public required ulong[] CommonGuilds { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs index 04628c5a..c13237e4 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs @@ -8,5 +8,5 @@ internal class GuildMemberUpdateEvent public ulong UserId { get; set; } [JsonPropertyName("nickname")] - public string Nickname { get; set; } + public required string Nickname { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/KMarkdownInfo.cs b/src/Kook.Net.WebSocket/API/Gateway/KMarkdownInfo.cs index 614b27be..8c145017 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/KMarkdownInfo.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/KMarkdownInfo.cs @@ -5,14 +5,14 @@ namespace Kook.API.Gateway; internal class KMarkdownInfo { [JsonPropertyName("raw_content")] - public string RawContent { get; set; } + public string? RawContent { get; set; } [JsonPropertyName("mention")] - public ulong[] MentionedUserIds { get; set; } + public ulong[]? MentionedUserIds { get; set; } [JsonPropertyName("mention_part")] - public MentionedUser[] MentionedUsers { get; set; } + public required MentionedUser[] MentionedUsers { get; set; } [JsonPropertyName("item_part")] - public Poke[] Pokes { get; set; } + public Poke[]? Pokes { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/MessageButtonClickEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/MessageButtonClickEvent.cs index c9f24e8b..527cc840 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/MessageButtonClickEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/MessageButtonClickEvent.cs @@ -1,11 +1,12 @@ using System.Text.Json.Serialization; +using Kook.API.Rest; namespace Kook.API.Gateway; internal class MessageButtonClickEvent { [JsonPropertyName("value")] - public string Value { get; set; } + public required string Value { get; set; } [JsonPropertyName("msg_id")] public Guid MessageId { get; set; } @@ -16,8 +17,11 @@ internal class MessageButtonClickEvent [JsonPropertyName("target_id")] public ulong ChannelId { get; set; } + [JsonPropertyName("channel_type")] + public required string ChannelType { get; set; } + [JsonPropertyName("user_info")] - public User User { get; set; } + public required GuildMember User { get; set; } [JsonPropertyName("guild_id")] public ulong? GuildId { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Gateway/PinnedMessageEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/MessagePinEvent.cs similarity index 90% rename from src/Kook.Net.WebSocket/API/Gateway/PinnedMessageEvent.cs rename to src/Kook.Net.WebSocket/API/Gateway/MessagePinEvent.cs index 25344fd7..56c5ee83 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/PinnedMessageEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/MessagePinEvent.cs @@ -2,7 +2,7 @@ namespace Kook.API.Gateway; -internal class PinnedMessageEvent +internal class MessagePinEvent { [JsonPropertyName("channel_id")] public ulong ChannelId { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Gateway/MessageUpdateEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/MessageUpdateEvent.cs index a393a382..bc8f126e 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/MessageUpdateEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/MessageUpdateEvent.cs @@ -9,24 +9,35 @@ internal class MessageUpdateEvent public ulong ChannelId { get; set; } [JsonPropertyName("content")] - public string Content { get; set; } + public required string Content { get; set; } [JsonPropertyName("mention")] - public ulong[] Mention { get; set; } + public ulong[]? MentionedUsers { get; set; } [JsonPropertyName("mention_all")] - public bool MentionAll { get; set; } + public bool MentionedAll { get; set; } [JsonPropertyName("mention_here")] - public bool MentionHere { get; set; } + public bool MentionedHere { get; set; } [JsonPropertyName("mention_roles")] - public uint[] MentionRoles { get; set; } + public required uint[] MentionedRoles { get; set; } [JsonPropertyName("updated_at")] [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))] public DateTimeOffset UpdatedAt { get; set; } + [JsonPropertyName("kmarkdown")] + public required MentionInfo MentionInfo { get; set; } + + [JsonPropertyName("quote")] + [JsonConverter(typeof(QuoteConverter))] + public Quote? Quote { get; set; } + + [JsonPropertyName("attachments")] + [JsonConverter(typeof(SafeAttachmentConverter))] + public Attachment? Attachment { get; set; } + [JsonPropertyName("msg_id")] public Guid MessageId { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/PrivateReaction.cs b/src/Kook.Net.WebSocket/API/Gateway/PrivateReaction.cs index 41d35a4c..c715247c 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/PrivateReaction.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/PrivateReaction.cs @@ -16,5 +16,5 @@ internal class PrivateReaction public ulong UserId { get; set; } [JsonPropertyName("emoji")] - public Emoji Emoji { get; set; } + public required Emoji Emoji { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/Reaction.cs b/src/Kook.Net.WebSocket/API/Gateway/Reaction.cs index f517f22c..7d14124f 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/Reaction.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/Reaction.cs @@ -14,5 +14,5 @@ internal class Reaction public ulong UserId { get; set; } [JsonPropertyName("emoji")] - public Emoji Emoji { get; set; } + public required Emoji Emoji { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Gateway/UnpinnedMessageEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/UnpinnedMessageEvent.cs deleted file mode 100644 index 11fefc6d..00000000 --- a/src/Kook.Net.WebSocket/API/Gateway/UnpinnedMessageEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Kook.API.Gateway; - -internal class UnpinnedMessageEvent -{ - [JsonPropertyName("channel_id")] - public ulong ChannelId { get; set; } - - [JsonPropertyName("operator_id")] - public ulong OperatorUserId { get; set; } - - [JsonPropertyName("msg_id")] - public Guid MessageId { get; set; } -} diff --git a/src/Kook.Net.WebSocket/API/Gateway/UserUpdateEvent.cs b/src/Kook.Net.WebSocket/API/Gateway/UserUpdateEvent.cs index be735046..06c639f4 100644 --- a/src/Kook.Net.WebSocket/API/Gateway/UserUpdateEvent.cs +++ b/src/Kook.Net.WebSocket/API/Gateway/UserUpdateEvent.cs @@ -8,8 +8,8 @@ internal class UserUpdateEvent public ulong UserId { get; set; } [JsonPropertyName("username")] - public string Username { get; set; } + public required string Username { get; set; } [JsonPropertyName("avatar")] - public string Avatar { get; set; } + public required string Avatar { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportParams.cs b/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportParams.cs index 7b229662..3c812ee2 100644 --- a/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportParams.cs +++ b/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportParams.cs @@ -5,11 +5,11 @@ namespace Kook.API.Voice; internal class CreatePlainTransportParams { [JsonPropertyName("comedia")] - public bool Comedia { get; set; } + public required bool Comedia { get; set; } [JsonPropertyName("rtcpMux")] - public bool RtcpMultiplexing { get; set; } + public required bool RtcpMultiplexing { get; set; } [JsonPropertyName("type")] - public string Type { get; set; } + public required string Type { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportResponse.cs b/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportResponse.cs index 283eaf6d..e4dd8972 100644 --- a/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportResponse.cs +++ b/src/Kook.Net.WebSocket/API/Voice/CreatePlainTransportResponse.cs @@ -8,7 +8,7 @@ internal class CreatePlainTransportResponse public Guid Id { get; set; } [JsonPropertyName("ip")] - public string Ip { get; set; } + public required string Ip { get; set; } [JsonPropertyName("port")] public int Port { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Voice/JoinParams.cs b/src/Kook.Net.WebSocket/API/Voice/JoinParams.cs index af1601d1..2510e81a 100644 --- a/src/Kook.Net.WebSocket/API/Voice/JoinParams.cs +++ b/src/Kook.Net.WebSocket/API/Voice/JoinParams.cs @@ -5,5 +5,5 @@ namespace Kook.API.Voice; internal class JoinParams { [JsonPropertyName("displayName")] - public string DisplayName { get; set; } + public required string DisplayName { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Voice/ProduceParams.cs b/src/Kook.Net.WebSocket/API/Voice/ProduceParams.cs index 8e913635..a3b55806 100644 --- a/src/Kook.Net.WebSocket/API/Voice/ProduceParams.cs +++ b/src/Kook.Net.WebSocket/API/Voice/ProduceParams.cs @@ -5,56 +5,56 @@ namespace Kook.API.Voice; internal class ProduceParams { [JsonPropertyName("appData")] - public object AppData { get; set; } + public required object AppData { get; set; } [JsonPropertyName("kind")] - public string Kind { get; set; } + public required string Kind { get; set; } [JsonPropertyName("peerId")] - public string PeerId { get; set; } + public required string PeerId { get; set; } [JsonPropertyName("rtpParameters")] - public RtpParameters RtpParameters { get; set; } + public required RtpParameters RtpParameters { get; set; } [JsonPropertyName("transportId")] - public Guid TransportId { get; set; } + public required Guid TransportId { get; set; } } internal class RtpParameters { [JsonPropertyName("codecs")] - public CodecParams[] Codecs { get; set; } + public required CodecParams[] Codecs { get; set; } [JsonPropertyName("encodings")] - public EncodingParams[] Encodings { get; set; } + public required EncodingParams[] Encodings { get; set; } } internal class EncodingParams { [JsonPropertyName("ssrc")] - public uint Ssrc { get; set; } + public required uint Ssrc { get; set; } } internal class CodecParams { [JsonPropertyName("channels")] - public int Channels { get; set; } + public required int Channels { get; set; } [JsonPropertyName("clockRate")] - public int ClockRate { get; set; } + public required int ClockRate { get; set; } [JsonPropertyName("mimeType")] - public string MimeType { get; set; } + public required string MimeType { get; set; } [JsonPropertyName("parameters")] - public Parameters Parameters { get; set; } + public required Parameters Parameters { get; set; } [JsonPropertyName("payloadType")] - public int PayloadType { get; set; } + public required int PayloadType { get; set; } } internal class Parameters { [JsonPropertyName("sprop-stereo")] - public int SenderProduceStereo { get; set; } + public required int SenderProduceStereo { get; set; } } diff --git a/src/Kook.Net.WebSocket/API/Voice/VoiceSocketIncomeFrame.cs b/src/Kook.Net.WebSocket/API/Voice/VoiceSocketIncomeFrame.cs index 91323d45..b400d646 100644 --- a/src/Kook.Net.WebSocket/API/Voice/VoiceSocketIncomeFrame.cs +++ b/src/Kook.Net.WebSocket/API/Voice/VoiceSocketIncomeFrame.cs @@ -15,7 +15,7 @@ internal class VoiceSocketIncomeFrame public bool Okay { get; set; } [JsonPropertyName("data")] - public object Payload { get; set; } + public required object Payload { get; set; } [JsonPropertyName("notification")] public bool Notification { get; set; } diff --git a/src/Kook.Net.WebSocket/API/Voice/VoiceSocketRequestFrame.cs b/src/Kook.Net.WebSocket/API/Voice/VoiceSocketRequestFrame.cs index 72e045b9..46df4f18 100644 --- a/src/Kook.Net.WebSocket/API/Voice/VoiceSocketRequestFrame.cs +++ b/src/Kook.Net.WebSocket/API/Voice/VoiceSocketRequestFrame.cs @@ -6,15 +6,15 @@ namespace Kook.API.Voice; internal class VoiceSocketRequestFrame { [JsonPropertyName("request")] - public bool Request { get; set; } + public required bool Request { get; set; } [JsonPropertyName("id")] - public uint Id { get; set; } + public required uint Id { get; set; } [JsonPropertyName("method")] [JsonConverter(typeof(VoiceSocketFrameTypeConverter))] - public VoiceSocketFrameType Type { get; set; } + public required VoiceSocketFrameType Type { get; set; } [JsonPropertyName("data")] - public object Payload { get; set; } + public required object Payload { get; set; } } diff --git a/src/Kook.Net.WebSocket/Audio/AudioClient.cs b/src/Kook.Net.WebSocket/Audio/AudioClient.cs index 91739ae2..2d3bafd8 100644 --- a/src/Kook.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Kook.Net.WebSocket/Audio/AudioClient.cs @@ -10,7 +10,7 @@ namespace Kook.Audio; //TODO: Add audio reconnecting internal partial class AudioClient : IAudioClient { - private static readonly int ConnectionTimeoutMs = 30000; // 30 seconds + private const int ConnectionTimeoutMs = 30000; // 30 seconds private readonly Random _ssrcRandom; @@ -18,7 +18,7 @@ internal partial class AudioClient : IAudioClient private readonly ConnectionManager _connection; private readonly SemaphoreSlim _stateLock; - private Task /*_heartbeatTask, _keepaliveTask, */_rtcpTask; + private Task? /*_heartbeatTask, _keepaliveTask, */_rtcpTask; private long _lastRtcpTime; private uint _sequence; private uint _ssrc; @@ -49,7 +49,8 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) _audioLogger = Kook.LogManager.CreateLogger($"Audio #{clientId}"); ApiClient = new KookVoiceAPIClient(guild.Id, Kook.WebSocketProvider, Kook.UdpSocketProvider); - ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.SentGatewayMessage += async opCode => + await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.ReceivedPacket += ProcessPacketAsync; @@ -64,16 +65,13 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) _sequence = 1000000; // LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); - UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); + UdpLatencyUpdated += async (old, val) => + await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } - internal Task StartAsync() - { - return _connection.StartAsync(); - } + internal Task StartAsync() => _connection.StartAsync(); - public Task StopAsync() - => _connection.StopAsync(); + public Task StopAsync() => _connection.StopAsync(); private async Task OnConnectingAsync() { @@ -121,32 +119,32 @@ private async Task OnDisconnectingAsync(Exception ex) /// public AudioOutStream CreateOpusStream(int bufferMillis = 1000) { - var outputStream = new OutputStream(ApiClient); //Ignores header - var rtpWriter = new RtpWriteStream(outputStream, _ssrc); //Consumes header, passes + OutputStream outputStream = new(ApiClient); //Ignores header + RtpWriteStream rtpWriter = new(outputStream, _ssrc); //Consumes header, passes return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancellationToken, _audioLogger); //Generates header } /// public AudioOutStream CreateDirectOpusStream() { - var outputStream = new OutputStream(ApiClient); //Ignores header - return new RtpWriteStream(outputStream, _ssrc); //Consumes header, passes + OutputStream outputStream = new(ApiClient); //Ignores header + return new RtpWriteStream(outputStream, _ssrc); //Consumes header, passes } /// public AudioOutStream CreatePcmStream(AudioApplication application, int bitrate = 96 * 1024, int bufferMillis = 1000, int packetLoss = 30) { - var outputStream = new OutputStream(ApiClient); - var rtpWriter = new RtpWriteStream(outputStream, _ssrc); - var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancellationToken, _audioLogger); //Ignores header, generates header + OutputStream outputStream = new(ApiClient); + RtpWriteStream rtpWriter = new(outputStream, _ssrc); + BufferedWriteStream bufferedStream = new(rtpWriter, this, bufferMillis, _connection.CancellationToken, _audioLogger); //Ignores header, generates header return new OpusEncodeStream(bufferedStream, bitrate, application, packetLoss); } /// public AudioOutStream CreateDirectPcmStream(AudioApplication application, int bitrate = 96 * 1024, int packetLoss = 30) { - var outputStream = new OutputStream(ApiClient); - var rtpWriter = new RtpWriteStream(outputStream, _ssrc); + OutputStream outputStream = new(ApiClient); + RtpWriteStream rtpWriter = new(outputStream, _ssrc); return new OpusEncodeStream(rtpWriter, bitrate, application, packetLoss); } @@ -164,85 +162,94 @@ private async Task ProcessMessageAsync(VoiceSocketFrameType type, bool okay, obj switch (type) { case VoiceSocketFrameType.GetRouterRtpCapabilities: - { - await _audioLogger.DebugAsync("RouterRtpCapabilities Completed").ConfigureAwait(false); - uint nextSequence = _sequence++; - await ApiClient.SendJoinRequestAsync(nextSequence).ConfigureAwait(false); - } + { + await _audioLogger.DebugAsync("RouterRtpCapabilities Completed").ConfigureAwait(false); + uint nextSequence = _sequence++; + await ApiClient.SendJoinRequestAsync(nextSequence).ConfigureAwait(false); + } break; case VoiceSocketFrameType.Join: - { - await _audioLogger.DebugAsync("Join Completed").ConfigureAwait(false); - uint nextSequence = _sequence++; - await ApiClient.SendCreatePlainTransportRequestAsync(nextSequence) - .ConfigureAwait(false); - } + { + await _audioLogger.DebugAsync("Join Completed").ConfigureAwait(false); + uint nextSequence = _sequence++; + await ApiClient.SendCreatePlainTransportRequestAsync(nextSequence).ConfigureAwait(false); + } break; case VoiceSocketFrameType.CreatePlainTransport: + { + await _audioLogger.DebugAsync("CreatePlainTransport Completed").ConfigureAwait(false); + string? json = payload.ToString(); + if (json is null + || JsonSerializer.Deserialize(json) is not { } data) { - await _audioLogger.DebugAsync("CreatePlainTransport Completed").ConfigureAwait(false); - CreatePlainTransportResponse data = - JsonSerializer.Deserialize(payload.ToString()); - ApiClient.SetUdpEndpoint(data.Ip, data.Port); - ApiClient.SetRtcpUdpEndpoint(data.Ip, data.RtcpPort); - uint nextSequence = _sequence++; - _ssrc = (uint)_ssrcRandom.Next(0, int.MaxValue); - await ApiClient.SendProduceRequestAsync(nextSequence, Kook.CurrentUser.Id, data.Id, _ssrc) - .ConfigureAwait(false); + _connection.Error(new Exception($"Unable to parse CreatePlainTransportResponse: {json}")); + break; } - break; - case VoiceSocketFrameType.Produce: + ApiClient.SetUdpEndpoint(data.Ip, data.Port); + ApiClient.SetRtcpUdpEndpoint(data.Ip, data.RtcpPort); + uint nextSequence = _sequence++; + _ssrc = (uint)_ssrcRandom.Next(0, int.MaxValue); + if (Kook.CurrentUser is null) { - await _audioLogger.DebugAsync("Produce Completed").ConfigureAwait(false); - _ = _connection.CompleteAsync(); - // int intervalMillis = KookSocketConfig.HeartbeatIntervalMilliseconds; - // _heartbeatTask = RunHeartbeatAsync(intervalMillis, _connection.CancellationToken); - // _keepaliveTask = RunKeepaliveAsync(_connection.CancellationToken); - _rtcpTask = RunRtcpAsync(KookSocketConfig.RtcpIntervalMilliseconds, - _connection.CancellationToken); + _connection.CriticalError(new Exception("The client is not logged in")); + break; } + await ApiClient + .SendProduceRequestAsync(nextSequence, Kook.CurrentUser.Id, data.Id, _ssrc) + .ConfigureAwait(false); + } + break; + case VoiceSocketFrameType.Produce: + { + await _audioLogger.DebugAsync("Produce Completed").ConfigureAwait(false); + _ = _connection.CompleteAsync(); + // int intervalMillis = KookSocketConfig.HeartbeatIntervalMilliseconds; + // _heartbeatTask = RunHeartbeatAsync(intervalMillis, _connection.CancellationToken); + // _keepaliveTask = RunKeepaliveAsync(_connection.CancellationToken); + _rtcpTask = RunRtcpAsync(KookSocketConfig.RtcpIntervalMilliseconds, _connection.CancellationToken); + } break; case VoiceSocketFrameType.NewPeer: - { - if (payload is not JsonElement jsonElement - || !jsonElement.TryGetProperty("id", out JsonElement idElement) - || !ulong.TryParse(idElement.ToString(), out ulong id)) - break; - await _peerConnectedEvent.InvokeAsync(id).ConfigureAwait(false); - await _audioLogger.DebugAsync("Received NewPeer").ConfigureAwait(false); - } + { + if (payload is not JsonElement jsonElement + || !jsonElement.TryGetProperty("id", out JsonElement idElement) + || !ulong.TryParse(idElement.ToString(), out ulong id)) + break; + await _peerConnectedEvent.InvokeAsync(id).ConfigureAwait(false); + await _audioLogger.DebugAsync("Received NewPeer").ConfigureAwait(false); + } break; case VoiceSocketFrameType.PeerClosed: - { - if (payload is not JsonElement jsonElement) - break; - if (!jsonElement.TryGetProperty("peerId", out JsonElement idElement) - || !ulong.TryParse(idElement.ToString(), out ulong id)) - break; - if (!jsonElement.TryGetProperty("fromId", out JsonElement fromIdElement) - || !ulong.TryParse(fromIdElement.ToString(), out ulong fromId) - || fromId != ChannelId) - break; - - await _peerDisconnectedEvent.InvokeAsync(id).ConfigureAwait(false); - await _audioLogger.DebugAsync("Received PeerClosed").ConfigureAwait(false); - } + { + if (payload is not JsonElement jsonElement) + break; + if (!jsonElement.TryGetProperty("peerId", out JsonElement idElement) + || !ulong.TryParse(idElement.ToString(), out ulong id)) + break; + if (!jsonElement.TryGetProperty("fromId", out JsonElement fromIdElement) + || !ulong.TryParse(fromIdElement.ToString(), out ulong fromId) + || fromId != ChannelId) + break; + + await _peerDisconnectedEvent.InvokeAsync(id).ConfigureAwait(false); + await _audioLogger.DebugAsync("Received PeerClosed").ConfigureAwait(false); + } break; case VoiceSocketFrameType.ResumeHeadset: case VoiceSocketFrameType.PauseHeadset: case VoiceSocketFrameType.ConsumerResumed: case VoiceSocketFrameType.ConsumerPaused: case VoiceSocketFrameType.PeerPermissionChanged: - { - await _audioLogger.DebugAsync(type.ToString()).ConfigureAwait(false); - } + { + await _audioLogger.DebugAsync(type.ToString()).ConfigureAwait(false); + } break; case VoiceSocketFrameType.Disconnect: - { - await Guild.DisconnectAudioAsync().ConfigureAwait(false); - await _clientDisconnectedEvent.InvokeAsync().ConfigureAwait(false); - await _audioLogger.DebugAsync("Received Disconnect").ConfigureAwait(false); - } + { + await Guild.DisconnectAudioAsync().ConfigureAwait(false); + await _clientDisconnectedEvent.InvokeAsync().ConfigureAwait(false); + await _audioLogger.DebugAsync("Received Disconnect").ConfigureAwait(false); + } break; default: await _audioLogger.WarningAsync($"Unknown VoiceSocketFrameType ({type})").ConfigureAwait(false); diff --git a/src/Kook.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Kook.Net.WebSocket/Audio/Opus/OpusConverter.cs index be2be66e..fc370e20 100644 --- a/src/Kook.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Kook.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -42,12 +42,12 @@ internal abstract class OpusConverter : IDisposable /// public const int FrameBytes = FrameSamplesPerChannel * SampleBytes; - protected bool _isDisposed = false; + protected bool IsDisposed = false; protected virtual void Dispose(bool disposing) { - if (!_isDisposed) - _isDisposed = true; + if (!IsDisposed) + IsDisposed = true; } ~OpusConverter() diff --git a/src/Kook.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Kook.Net.WebSocket/Audio/Opus/OpusEncoder.cs index e04064dd..448f3896 100644 --- a/src/Kook.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Kook.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -59,20 +59,25 @@ public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int { int result = 0; fixed (byte* inPtr = input) - fixed (byte* outPtr = output) - result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, - output.Length - outputOffset); + { + fixed (byte* outPtr = output) + { + result = Encode(_ptr, + inPtr + inputOffset, + FrameSamplesPerChannel, outPtr + outputOffset, + output.Length - outputOffset); + } + } + CheckError(result); return result; } protected override void Dispose(bool disposing) { - if (!_isDisposed) - { - if (_ptr != IntPtr.Zero) - DestroyEncoder(_ptr); - base.Dispose(disposing); - } + if (IsDisposed) return; + if (_ptr != IntPtr.Zero) + DestroyEncoder(_ptr); + base.Dispose(disposing); } } diff --git a/src/Kook.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Kook.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index f0938d27..5ae0434d 100644 --- a/src/Kook.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Kook.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -20,7 +20,6 @@ private struct Frame(byte[] buffer, int bytes) private readonly AudioStream _next; private readonly CancellationTokenSource _disposeTokenSource, _cancellationTokenSource; private readonly CancellationToken _cancellationToken; - private readonly Task _task; private readonly ConcurrentQueue _queuedFrames; private readonly ConcurrentQueue _bufferPool; private readonly SemaphoreSlim _queueLock; @@ -29,7 +28,8 @@ private struct Frame(byte[] buffer, int bytes) private bool _isPreloaded; // private int _silenceFrames; - internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMillis, CancellationToken cancellationToken, Logger logger, int maxFrameSize = 1500) + internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMillis, + CancellationToken cancellationToken, Logger logger, int maxFrameSize = 1500) { //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; @@ -48,7 +48,7 @@ internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMil _queueLock = new SemaphoreSlim(_queueLength, _queueLength); // _silenceFrames = MaxSilenceFrames; - _task = Run(); + _ = Run(); } /// @@ -139,7 +139,7 @@ public override void WriteHeader(ushort seq, uint timestamp, bool missed) /// public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - CancellationTokenSource writeCancellationToken = null; + CancellationTokenSource? writeCancellationToken = null; if (cancellationToken.CanBeCanceled) { writeCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken); @@ -149,14 +149,12 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc cancellationToken = _cancellationToken; await _queueLock.WaitAsync(-1, cancellationToken).ConfigureAwait(false); - if (!_bufferPool.TryDequeue(out byte[] dstBuffer)) + if (!_bufferPool.TryDequeue(out byte[]? dstBuffer)) { #if DEBUG _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock #endif -#pragma warning disable IDISP016 writeCancellationToken?.Dispose(); -#pragma warning restore IDISP016 return; } Buffer.BlockCopy(buffer, offset, dstBuffer, 0, count); @@ -177,7 +175,7 @@ public override async Task FlushAsync(CancellationToken cancellationToken) while (true) { cancellationToken.ThrowIfCancellationRequested(); - if (_queuedFrames.Count == 0) + if (_queuedFrames.IsEmpty) return; await Task.Delay(250, cancellationToken).ConfigureAwait(false); } diff --git a/src/Kook.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Kook.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index 0aa22123..9f9cb586 100644 --- a/src/Kook.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Kook.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -110,12 +110,12 @@ public override async Task FlushAsync(CancellationToken cancellationToken) }*/ /// - public override Task FlushAsync(CancellationToken cancellationToken) - => _next.FlushAsync(cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) => + _next.FlushAsync(cancellationToken); /// - public override Task ClearAsync(CancellationToken cancellationToken) - => _next.ClearAsync(cancellationToken); + public override Task ClearAsync(CancellationToken cancellationToken) => + _next.ClearAsync(cancellationToken); /// protected override void Dispose(bool disposing) diff --git a/src/Kook.Net.WebSocket/Audio/Streams/RtpWriteStream.cs b/src/Kook.Net.WebSocket/Audio/Streams/RtpWriteStream.cs index 040827d3..47f176fb 100644 --- a/src/Kook.Net.WebSocket/Audio/Streams/RtpWriteStream.cs +++ b/src/Kook.Net.WebSocket/Audio/Streams/RtpWriteStream.cs @@ -70,16 +70,16 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati Buffer.BlockCopy(buffer, offset, _buffer, 12, count); _next.WriteHeader(_nextSeq, _nextTimestamp, false); - return _next.WriteAsync(_buffer, 0, count + 12); + return _next.WriteAsync(_buffer, 0, count + 12, cancellationToken); } /// - public override Task FlushAsync(CancellationToken cancellationToken) - => _next.FlushAsync(cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) => + _next.FlushAsync(cancellationToken); /// - public override Task ClearAsync(CancellationToken cancellationToken) - => _next.ClearAsync(cancellationToken); + public override Task ClearAsync(CancellationToken cancellationToken) => + _next.ClearAsync(cancellationToken); /// protected override void Dispose(bool disposing) diff --git a/src/Kook.Net.WebSocket/BaseSocketClient.Event.cs b/src/Kook.Net.WebSocket/BaseSocketClient.Event.cs index 07fcdd4e..5e228fa8 100644 --- a/src/Kook.Net.WebSocket/BaseSocketClient.Event.cs +++ b/src/Kook.Net.WebSocket/BaseSocketClient.Event.cs @@ -1,3 +1,5 @@ +using Kook.Rest; + namespace Kook.WebSocket; public abstract partial class BaseSocketClient @@ -170,13 +172,13 @@ public event Func, SocketTextChannel, Cacheable /// - public event Func, Cacheable, Cacheable, SocketReaction, Task> DirectReactionAdded + public event Func, Cacheable, Cacheable, SocketReaction, Task> DirectReactionAdded { add => _directReactionAddedEvent.Add(value); remove => _directReactionAddedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, Cacheable, SocketReaction, Task>> _directReactionAddedEvent = new(); + internal readonly AsyncEvent, Cacheable, Cacheable, SocketReaction, Task>> _directReactionAddedEvent = new(); /// Fired when a reaction is removed from a message. /// @@ -209,13 +211,13 @@ public event Func, Cacheable, Cachea /// information. /// /// - public event Func, Cacheable, Cacheable, SocketReaction, Task> DirectReactionRemoved + public event Func, Cacheable, Cacheable, SocketReaction, Task> DirectReactionRemoved { add => _directReactionRemovedEvent.Add(value); remove => _directReactionRemovedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, Cacheable, SocketReaction, Task>> _directReactionRemovedEvent = new(); + internal readonly AsyncEvent, Cacheable, Cacheable, SocketReaction, Task>> _directReactionRemovedEvent = new(); #endregion @@ -311,13 +313,13 @@ public event Func, SocketTextChannel, Task> MessageDel /// parameter. /// /// - public event Func, Cacheable, SocketTextChannel, Task> MessageUpdated + public event Func, Cacheable, SocketTextChannel, Task> MessageUpdated { add => _messageUpdatedEvent.Add(value); remove => _messageUpdatedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, SocketTextChannel, Task>> _messageUpdatedEvent = new(); + internal readonly AsyncEvent, Cacheable, SocketTextChannel, Task>> _messageUpdatedEvent = new(); /// Fired when a message is pinned. /// @@ -354,13 +356,13 @@ public event Func, Cacheable /// is preserved in the . /// /// - public event Func, Cacheable, SocketTextChannel, Cacheable, Task> MessagePinned + public event Func, Cacheable, SocketTextChannel, Cacheable, Task> MessagePinned { add => _messagePinnedEvent.Add(value); remove => _messagePinnedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, SocketTextChannel, Cacheable, Task>> _messagePinnedEvent = new(); + internal readonly AsyncEvent, Cacheable, SocketTextChannel, Cacheable, Task>> _messagePinnedEvent = new(); /// Fired when a message is unpinned. /// @@ -397,13 +399,13 @@ public event Func, Cacheable /// is preserved in the . /// /// - public event Func, Cacheable, SocketTextChannel, Cacheable, Task> MessageUnpinned + public event Func, Cacheable, SocketTextChannel, Cacheable, Task> MessageUnpinned { add => _messageUnpinnedEvent.Add(value); remove => _messageUnpinnedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, SocketTextChannel, Cacheable, Task>> _messageUnpinnedEvent = new(); + internal readonly AsyncEvent, Cacheable, SocketTextChannel, Cacheable, Task>> _messageUnpinnedEvent = new(); #endregion @@ -510,13 +512,13 @@ public event Func, Cacheable, Cache /// otherwise, the direct message channel has not been created yet, and the as chat code will be preserved. /// /// - public event Func, Cacheable, Cacheable, Cacheable, Task> DirectMessageUpdated + public event Func, Cacheable, Cacheable, Cacheable, Task> DirectMessageUpdated { add => _directMessageUpdatedEvent.Add(value); remove => _directMessageUpdatedEvent.Remove(value); } - internal readonly AsyncEvent, Cacheable, Cacheable, Cacheable, Task>> _directMessageUpdatedEvent = new(); + internal readonly AsyncEvent, Cacheable, Cacheable, Cacheable, Task>> _directMessageUpdatedEvent = new(); #endregion @@ -539,13 +541,13 @@ public event Func, Cacheable, Cac /// The time at which the user joined the guild will be passed into the parameter. /// /// - public event Func UserJoined + public event Func, DateTimeOffset, Task> UserJoined { add => _userJoinedEvent.Add(value); remove => _userJoinedEvent.Remove(value); } - internal readonly AsyncEvent> _userJoinedEvent = new(); + internal readonly AsyncEvent, DateTimeOffset, Task>> _userJoinedEvent = new(); /// Fired when a user leaves a guild. /// @@ -594,7 +596,7 @@ public event Func, DateTimeOffset, Tas /// /// /// The users who operated the bans is passed into the event handler parameter as - /// , which contains a when the user + /// , which contains a when the user /// presents in the cache; otherwise, in event that the user cannot be retrieved, the ID of the user /// is preserved in the . /// @@ -606,13 +608,13 @@ public event Func, DateTimeOffset, Tas /// The reason of the ban is passed into the event handler parameter as string. /// /// - public event Func>, Cacheable, SocketGuild, string, Task> UserBanned + public event Func>, Cacheable, SocketGuild, string?, Task> UserBanned { add => _userBannedEvent.Add(value); remove => _userBannedEvent.Remove(value); } - internal readonly AsyncEvent>, Cacheable, SocketGuild, string, Task>> _userBannedEvent = new(); + internal readonly AsyncEvent>, Cacheable, SocketGuild, string?, Task>> _userBannedEvent = new(); /// Fired when a user is unbanned from a guild. /// @@ -634,7 +636,7 @@ public event Func>, Cacheable /// /// The users who operated the unbans is passed into the event handler parameter as - /// , which contains a when the user + /// , which contains a when the user /// presents in the cache; otherwise, in event that the user cannot be retrieved, the ID of the user /// is preserved in the . /// @@ -643,13 +645,13 @@ public event Func>, Cacheable. /// /// - public event Func>, Cacheable, SocketGuild, Task> UserUnbanned + public event Func>, Cacheable, SocketGuild, Task> UserUnbanned { add => _userUnbannedEvent.Add(value); remove => _userUnbannedEvent.Remove(value); } - internal readonly AsyncEvent>, Cacheable, SocketGuild, Task>> _userUnbannedEvent = new(); + internal readonly AsyncEvent>, Cacheable, SocketGuild, Task>> _userUnbannedEvent = new(); /// Fired when a user is updated. /// @@ -997,13 +999,13 @@ public event Func EmoteDeleted /// . /// /// - public event Func EmoteUpdated + public event Func EmoteUpdated { add => _emoteUpdatedEvent.Add(value); remove => _emoteUpdatedEvent.Remove(value); } - internal readonly AsyncEvent> _emoteUpdatedEvent = new(); + internal readonly AsyncEvent> _emoteUpdatedEvent = new(); #endregion diff --git a/src/Kook.Net.WebSocket/BaseSocketClient.cs b/src/Kook.Net.WebSocket/BaseSocketClient.cs index 963eb8ea..20e89456 100644 --- a/src/Kook.Net.WebSocket/BaseSocketClient.cs +++ b/src/Kook.Net.WebSocket/BaseSocketClient.cs @@ -11,7 +11,7 @@ public abstract partial class BaseSocketClient : BaseKookClient, IKookClient /// /// Gets the configuration used by this client. /// - protected readonly KookSocketConfig _baseConfig; + protected readonly KookSocketConfig BaseConfig; /// /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. @@ -27,12 +27,13 @@ public abstract partial class BaseSocketClient : BaseKookClient, IKookClient /// public abstract KookSocketRestClient Rest { get; } - internal new KookSocketApiClient ApiClient => base.ApiClient as KookSocketApiClient; + internal new KookSocketApiClient ApiClient => base.ApiClient as KookSocketApiClient + ?? throw new InvalidOperationException("The API client is not a WebSocket-based client."); /// /// Gets the current logged-in user. /// - public new virtual SocketSelfUser CurrentUser + public new virtual SocketSelfUser? CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; @@ -47,7 +48,10 @@ public abstract partial class BaseSocketClient : BaseKookClient, IKookClient public abstract IReadOnlyCollection Guilds { get; } internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) - : base(config, client) => _baseConfig = config; + : base(config, client) + { + BaseConfig = config; + } /// /// Gets a generic user. @@ -70,7 +74,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// /// A generic WebSocket-based user; null when the user cannot be found. /// - public abstract SocketUser GetUser(ulong id); + public abstract SocketUser? GetUser(ulong id); /// /// Gets a user. @@ -94,7 +98,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// /// A generic WebSocket-based user; null when the user cannot be found. /// - public abstract SocketUser GetUser(string username, string identifyNumber); + public abstract SocketUser? GetUser(string username, string identifyNumber); /// /// Gets a channel. @@ -104,7 +108,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// A generic WebSocket-based channel object (voice, text, category, etc.) associated with the identifier; /// null when the channel cannot be found. /// - public abstract SocketChannel GetChannel(ulong id); + public abstract SocketChannel? GetChannel(ulong id); /// /// Gets a channel. @@ -114,7 +118,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// A generic WebSocket-based channel object (voice, text, category, etc.) associated with the identifier; /// null when the channel cannot be found. /// - public abstract SocketDMChannel GetDMChannel(Guid chatCode); + public abstract SocketDMChannel? GetDMChannel(Guid chatCode); /// /// Gets a channel. @@ -124,7 +128,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// A generic WebSocket-based channel object (voice, text, category, etc.) associated with the identifier; /// null when the channel cannot be found. /// - public abstract SocketDMChannel GetDMChannel(ulong userId); + public abstract SocketDMChannel? GetDMChannel(ulong userId); /// /// Gets a guild. @@ -134,7 +138,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// A WebSocket-based guild associated with the identifier; null when the guild cannot be /// found. /// - public abstract SocketGuild GetGuild(ulong id); + public abstract SocketGuild? GetGuild(ulong id); /// /// Starts the WebSocket connection. @@ -156,7 +160,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// /// A task that represents the asynchronous download operation. /// - public abstract Task DownloadUsersAsync(IEnumerable guilds = null, RequestOptions options = null); + public abstract Task DownloadUsersAsync(IEnumerable? guilds = null, RequestOptions? options = null); /// /// Downloads all voice states for the specified guilds. @@ -165,7 +169,7 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// The guilds to download the voice states for. If null, all available guilds will be downloaded. /// /// The options to be used when sending the request. - public abstract Task DownloadVoiceStatesAsync(IEnumerable guilds = null, RequestOptions options = null); + public abstract Task DownloadVoiceStatesAsync(IEnumerable? guilds = null, RequestOptions? options = null); /// /// Downloads all boost subscriptions for the specified guilds. @@ -176,34 +180,36 @@ internal BaseSocketClient(KookSocketConfig config, KookRestApiClient client) /// permission. /// /// The options to be used when sending the request. - public abstract Task DownloadBoostSubscriptionsAsync(IEnumerable guilds = null, RequestOptions options = null); + public abstract Task DownloadBoostSubscriptionsAsync(IEnumerable? guilds = null, + RequestOptions? options = null); /// - Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetChannel(id)); + Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetChannel(id)); /// - Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions options) - => Task.FromResult(GetDMChannel(chatCode)); + Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetDMChannel(chatCode)); /// - Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetGuild(id)); + Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetGuild(id)); /// - Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(Guilds); + Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(Guilds); /// - async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { - SocketUser user = GetUser(id); - if (user is not null || mode == CacheMode.CacheOnly) return user; - + if (GetUser(id) is { } user) + return user; + if (mode == CacheMode.CacheOnly) + return null; return await Rest.GetUserAsync(id, options).ConfigureAwait(false); } /// - Task IKookClient.GetUserAsync(string username, string identifyNumber, RequestOptions options) - => Task.FromResult(GetUser(username, identifyNumber)); + Task IKookClient.GetUserAsync(string username, string identifyNumber, RequestOptions? options) => + Task.FromResult(GetUser(username, identifyNumber)); } diff --git a/src/Kook.Net.WebSocket/ClientState.cs b/src/Kook.Net.WebSocket/ClientState.cs index b569e47b..8d7a3929 100644 --- a/src/Kook.Net.WebSocket/ClientState.cs +++ b/src/Kook.Net.WebSocket/ClientState.cs @@ -9,12 +9,12 @@ internal class ClientState private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth - private readonly ConcurrentDictionary _guildChannels; + private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; - internal IReadOnlyCollection GuildChannels => _guildChannels.ToReadOnlyCollection(); + internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); @@ -23,7 +23,7 @@ public ClientState(int guildCount, int dmChannelCount) { double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; - _guildChannels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, + _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); _dmChannels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); @@ -32,82 +32,61 @@ public ClientState(int guildCount, int dmChannelCount) (int)(estimatedUsersCount * CollectionMultiplier)); } - internal SocketChannel GetChannel(ulong chatCode) - { - if (_guildChannels.TryGetValue(chatCode, out SocketChannel channel)) return channel; - - return null; - } + internal SocketChannel? GetChannel(ulong channelId) => + _channels.TryGetValue(channelId, out SocketChannel? channel) ? channel : null; - internal SocketDMChannel GetDMChannel(Guid userId) - { - if (_dmChannels.TryGetValue(userId, out SocketDMChannel channel)) return channel; - - return null; - } + internal SocketDMChannel? GetDMChannel(Guid userId) => + _dmChannels.TryGetValue(userId, out SocketDMChannel? channel) ? channel : null; - internal SocketDMChannel GetDMChannel(ulong userId) - { - return _dmChannels.Values.FirstOrDefault(x => x.Recipient.Id == userId); - } + internal SocketDMChannel? GetDMChannel(ulong userId) => + _dmChannels.Values.FirstOrDefault(x => x.Recipient.Id == userId); - internal void AddChannel(SocketChannel channel) => _guildChannels[channel.Id] = channel; + internal void AddChannel(SocketChannel channel) => _channels[channel.Id] = channel; internal void AddDMChannel(SocketDMChannel channel) => _dmChannels[channel.Id] = channel; - internal SocketChannel RemoveChannel(ulong id) - { - if (_guildChannels.TryRemove(id, out SocketChannel channel)) return channel; - - return null; - } + internal SocketChannel? RemoveChannel(ulong id) => + _channels.TryRemove(id, out SocketChannel? channel) ? channel : null; internal void PurgeAllChannels() { - foreach (SocketGuild guild in _guilds.Values) guild.PurgeChannelCache(this); + foreach (SocketGuild guild in _guilds.Values) + guild.PurgeChannelCache(this); } internal void PurgeDMChannels() => _dmChannels.Clear(); - internal SocketGuild GetGuild(ulong id) - { - if (_guilds.TryGetValue(id, out SocketGuild guild)) return guild; - - return null; - } + internal SocketGuild? GetGuild(ulong id) => + _guilds.TryGetValue(id, out SocketGuild? guild) ? guild : null; internal void AddGuild(SocketGuild guild) => _guilds[guild.Id] = guild; - internal SocketGuild RemoveGuild(ulong id) + internal SocketGuild? RemoveGuild(ulong id) { - if (_guilds.TryRemove(id, out SocketGuild guild)) - { - guild.PurgeChannelCache(this); - guild.PurgeUserCache(); - return guild; - } - - return null; + if (!_guilds.TryRemove(id, out SocketGuild? guild)) + return null; + guild.PurgeChannelCache(this); + guild.PurgeUserCache(); + return guild; } - internal SocketGlobalUser GetUser(ulong id) - { - if (_users.TryGetValue(id, out SocketGlobalUser user)) return user; - - return null; - } + internal SocketGlobalUser? GetUser(ulong id) => + _users.TryGetValue(id, out SocketGlobalUser? user) ? user : null; - internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) => _users.GetOrAdd(id, userFactory); + internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) => + _users.GetOrAdd(id, userFactory); - internal SocketGlobalUser RemoveUser(ulong id) - { - if (_users.TryRemove(id, out SocketGlobalUser user)) return user; + internal SocketGlobalUser AddOrUpdateUser(ulong id, + Func addFactory, + Func updateFactory) => + _users.AddOrUpdate(id, addFactory, updateFactory); - return null; - } + internal SocketGlobalUser? RemoveUser(ulong id) => + _users.TryRemove(id, out SocketGlobalUser? user) ? user : null; internal void PurgeUsers() { - foreach (SocketGuild guild in _guilds.Values) guild.PurgeUserCache(); + foreach (SocketGuild guild in _guilds.Values) + guild.PurgeUserCache(); } } diff --git a/src/Kook.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Kook.Net.WebSocket/Commands/SocketCommandContext.cs index e71b5c0e..18f3c120 100644 --- a/src/Kook.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Kook.Net.WebSocket/Commands/SocketCommandContext.cs @@ -17,7 +17,7 @@ public class SocketCommandContext : ICommandContext /// /// Gets the that the command is executed in. /// - public SocketGuild Guild { get; } + public SocketGuild? Guild { get; } /// /// Gets the that the command is executed in. @@ -61,7 +61,7 @@ public SocketCommandContext(KookSocketClient client, SocketUserMessage msg) IKookClient ICommandContext.Client => Client; /// - IGuild ICommandContext.Guild => Guild; + IGuild? ICommandContext.Guild => Guild; /// IMessageChannel ICommandContext.Channel => Channel; diff --git a/src/Kook.Net.WebSocket/ConnectionManager.cs b/src/Kook.Net.WebSocket/ConnectionManager.cs index bb80ce43..1bf1dcdf 100644 --- a/src/Kook.Net.WebSocket/ConnectionManager.cs +++ b/src/Kook.Net.WebSocket/ConnectionManager.cs @@ -27,17 +27,20 @@ public event Func Disconnected private readonly Func _onConnecting; private readonly Func _onDisconnecting; - private TaskCompletionSource _connectionPromise, _readyPromise; - private CancellationTokenSource _combinedCancellationToken, _reconnectCancellationToken, _connectionCancellationToken; - private Task _task; + private TaskCompletionSource? _connectionPromise; + private TaskCompletionSource? _readyPromise; + private CancellationTokenSource? _combinedCancellationToken; + private CancellationTokenSource? _reconnectCancellationToken; + private CancellationTokenSource? _connectionCancellationToken; private bool _isDisposed; public ConnectionState State { get; private set; } public CancellationToken CancellationToken { get; private set; } - internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, - Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) + internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, + int connectionTimeout, Func onConnecting, + Func onDisconnecting, Action> clientDisconnectHandler) { _stateLock = stateLock; _logger = logger; @@ -49,7 +52,7 @@ internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectio { if (ex != null) { - WebSocketClosedException ex2 = ex as WebSocketClosedException; + WebSocketClosedException? ex2 = ex as WebSocketClosedException; if (ex2?.CloseCode == 4006) CriticalError(new Exception("WebSocket session expired", ex)); else if (ex2?.CloseCode == 4014) @@ -60,19 +63,20 @@ internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectio else Error(new Exception("WebSocket connection was closed")); - return Task.Delay(0); + return Task.CompletedTask; }); } public virtual async Task StartAsync() { - if (State != ConnectionState.Disconnected) throw new InvalidOperationException("Cannot start an already running client."); + if (State != ConnectionState.Disconnected) + throw new InvalidOperationException("Cannot start an already running client."); await AcquireConnectionLock().ConfigureAwait(false); CancellationTokenSource reconnectCancellationToken = new(); _reconnectCancellationToken?.Dispose(); _reconnectCancellationToken = reconnectCancellationToken; - _task = Task.Run(async () => + _ = Task.Run(async () => { try { @@ -84,7 +88,10 @@ public virtual async Task StartAsync() { await ConnectAsync(reconnectCancellationToken).ConfigureAwait(false); nextReconnectDelay = 1000; //Reset delay - await _connectionPromise.Task.ConfigureAwait(false); + if (_connectionPromise is null) + await _logger.ErrorAsync("The connection promise was null after connecting").ConfigureAwait(false); + else + await _connectionPromise.Task.ConfigureAwait(false); } catch (Exception ex) { @@ -106,7 +113,8 @@ public virtual async Task StartAsync() //Wait before reconnecting await Task.Delay(nextReconnectDelay, reconnectCancellationToken.Token).ConfigureAwait(false); nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250); - if (nextReconnectDelay > 60000) nextReconnectDelay = 60000; + if (nextReconnectDelay > 60000) + nextReconnectDelay = 60000; } } } @@ -114,7 +122,7 @@ public virtual async Task StartAsync() { _stateLock.Release(); } - }); + }, CancellationToken.None); } public virtual Task StopAsync() @@ -128,7 +136,8 @@ private async Task ConnectAsync(CancellationTokenSource reconnectCancellationTok _connectionCancellationToken?.Dispose(); _combinedCancellationToken?.Dispose(); _connectionCancellationToken = new CancellationTokenSource(); - _combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancellationToken.Token, reconnectCancellationToken.Token); + _combinedCancellationToken = CancellationTokenSource + .CreateLinkedTokenSource(_connectionCancellationToken.Token, reconnectCancellationToken.Token); CancellationToken = _combinedCancellationToken.Token; _connectionPromise = new TaskCompletionSource(); @@ -142,7 +151,7 @@ private async Task ConnectAsync(CancellationTokenSource reconnectCancellationTok //Abort connection on timeout CancellationToken cancellationToken = CancellationToken; - Task _ = Task.Run(async () => + _ = Task.Run(async () => { try { @@ -153,7 +162,7 @@ private async Task ConnectAsync(CancellationTokenSource reconnectCancellationTok { Console.WriteLine(e.Message); } - }); + }, CancellationToken.None); await _onConnecting().ConfigureAwait(false); @@ -183,13 +192,21 @@ private async Task DisconnectAsync(Exception ex, bool isReconnecting) await _logger.InfoAsync("Disconnected").ConfigureAwait(false); } - public Task CompleteAsync() + public async Task CompleteAsync() { - _readyPromise.TrySetResult(true); - return Task.CompletedTask; + if (_readyPromise is null) + await _logger.ErrorAsync("The ready promise was null when trying to complete the connection"); + else + _readyPromise.TrySetResult(true); } - public async Task WaitAsync() => await _readyPromise.Task.ConfigureAwait(false); + public async Task WaitAsync() + { + if (_readyPromise is null) + await _logger.ErrorAsync("The ready promise was null when trying to complete the connection"); + else + await _readyPromise.Task.ConfigureAwait(false); + } public void Cancel() { @@ -201,8 +218,8 @@ public void Cancel() public void Error(Exception ex) { - _readyPromise.TrySetException(ex); - _connectionPromise.TrySetException(ex); + _readyPromise?.TrySetException(ex); + _connectionPromise?.TrySetException(ex); _connectionCancellationToken?.Cancel(); } @@ -214,8 +231,8 @@ public void CriticalError(Exception ex) public void Reconnect() { - _readyPromise.TrySetCanceled(); - _connectionPromise.TrySetCanceled(); + _readyPromise?.TrySetCanceled(); + _connectionPromise?.TrySetCanceled(); _connectionCancellationToken?.Cancel(); } @@ -224,23 +241,22 @@ private async Task AcquireConnectionLock() while (true) { await StopAsync().ConfigureAwait(false); - if (await _stateLock.WaitAsync(0).ConfigureAwait(false)) break; + if (await _stateLock.WaitAsync(0, CancellationToken.None).ConfigureAwait(false)) + break; } } protected virtual void Dispose(bool disposing) { - if (!_isDisposed) + if (_isDisposed) return; + if (disposing) { - if (disposing) - { - _combinedCancellationToken?.Dispose(); - _reconnectCancellationToken?.Dispose(); - _connectionCancellationToken?.Dispose(); - } - - _isDisposed = true; + _combinedCancellationToken?.Dispose(); + _reconnectCancellationToken?.Dispose(); + _connectionCancellationToken?.Dispose(); } + + _isDisposed = true; } public void Dispose() => Dispose(true); diff --git a/src/Kook.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index a6a15cb8..39f55b7b 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -3,6 +3,4 @@ namespace Kook.WebSocket; /// /// Represents a generic WebSocket-based audio channel. /// -public interface ISocketAudioChannel : IAudioChannel -{ -} +public interface ISocketAudioChannel : IAudioChannel; diff --git a/src/Kook.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index ffc8ed7c..4180c943 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -32,7 +32,7 @@ public interface ISocketMessageChannel : IMessageChannel /// A WebSocket-based message object; null if it does not exist in the cache or if caching is not /// enabled. /// - SocketMessage GetCachedMessage(Guid id); + SocketMessage? GetCachedMessage(Guid id); /// /// Gets the last N cached messages from this message channel. diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index f3899b94..b1114fa5 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -8,16 +8,17 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based category channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { #region SocketCategoryChannel /// - public override IReadOnlyCollection Users - => Guild.Users.Where(x => Permissions.GetValue( + public override IReadOnlyCollection Users => Guild.Users + .Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), - ChannelPermission.ViewChannel)).ToImmutableArray(); + ChannelPermission.ViewChannel)) + .ToImmutableArray(); /// /// Gets the child channels of this category. @@ -26,12 +27,14 @@ public override IReadOnlyCollection Users /// A read-only collection of whose /// matches the identifier of this category channel. /// - public IReadOnlyCollection Channels - => Guild.Channels.Where(x => x is INestedChannel nestedChannel && nestedChannel.CategoryId == Id).ToImmutableArray(); + public IReadOnlyCollection Channels => + [..Guild.Channels.Where(x => x is INestedChannel nestedChannel && nestedChannel.CategoryId == Id)]; internal SocketCategoryChannel(KookSocketClient kook, ulong id, SocketGuild guild) - : base(kook, id, guild) => + : base(kook, id, guild) + { Type = ChannelType.Category; + } internal static new SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) { @@ -45,39 +48,35 @@ internal SocketCategoryChannel(KookSocketClient kook, ulong id, SocketGuild guil #region Users /// - public override SocketGuildUser GetUser(ulong id) + public override SocketGuildUser? GetUser(ulong id) { - SocketGuildUser user = Guild.GetUser(id); - if (user != null) - { - ulong guildPerms = Permissions.ResolveGuild(Guild, user); - ulong channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); - if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) return user; - } - - return null; + if (Guild.GetUser(id) is not { } user) return null; + ulong guildPerms = Permissions.ResolveGuild(Guild, user); + ulong channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + return Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel) ? user : null; } #endregion private string DebuggerDisplay => $"{Name} ({Id}, Category)"; - internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + internal new SocketCategoryChannel Clone() => (SocketCategoryChannel)MemberwiseClone(); #region IGuildChannel /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, - RequestOptions options) => + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => mode == CacheMode.AllowDownload ? ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options) : ImmutableArray.Create>(Users).ToAsyncEnumerable(); /// - async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { - SocketGuildUser user = GetUser(id); - if (user is not null || mode == CacheMode.CacheOnly) return user; - + if (GetUser(id) is { } user) + return user; + if (mode == CacheMode.CacheOnly) + return null; return await ChannelHelper.GetUserAsync(this, Guild, Kook, id, options).ConfigureAwait(false); } @@ -86,17 +85,18 @@ async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, Requ #region IChannel /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => mode == CacheMode.AllowDownload ? ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options) : ImmutableArray.Create>(Users).ToAsyncEnumerable(); /// - async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { - SocketGuildUser user = GetUser(id); - if (user is not null || mode == CacheMode.CacheOnly) return user; - + if (GetUser(id) is { } user) + return user; + if (mode == CacheMode.CacheOnly) + return null; return await ChannelHelper.GetUserAsync(this, Guild, Kook, id, options).ConfigureAwait(false); } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketChannel.cs index 608abd20..62d2a285 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -6,7 +6,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel, IUpdateable { #region SocketChannel @@ -24,7 +24,7 @@ internal SocketChannel(KookSocketClient kook, ulong id) internal abstract void Update(ClientState state, Model model); /// - public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); + public abstract Task UpdateAsync(RequestOptions? options = null); #endregion @@ -37,28 +37,28 @@ internal SocketChannel(KookSocketClient kook, ulong id) /// /// A generic WebSocket-based user associated with the identifier. /// - public SocketUser GetUser(ulong id) => GetUserInternal(id); + public SocketUser? GetUser(ulong id) => GetUserInternal(id); - internal abstract SocketUser GetUserInternal(ulong id); + internal abstract SocketUser? GetUserInternal(ulong id); internal abstract IReadOnlyCollection GetUsersInternal(); #endregion private string DebuggerDisplay => $"Unknown ({Id}, Channel)"; - internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; + internal SocketChannel Clone() => (SocketChannel)MemberwiseClone(); #region IChannel /// - string IChannel.Name => null; + string IChannel.Name => string.Empty; /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overridden + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(null); //Overridden /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + AsyncEnumerable.Empty>(); //Overridden #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index 7b2493a2..085e0ead 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -6,37 +6,32 @@ namespace Kook.WebSocket; internal static class SocketChannelHelper { - public static IAsyncEnumerable> GetMessagesAsync(ISocketMessageChannel channel, KookSocketClient kook, - MessageCache messages, - Guid? referenceMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + public static IAsyncEnumerable> GetMessagesAsync(ISocketMessageChannel channel, + KookSocketClient kook, MessageCache? messages, + Guid? referenceMessageId, Direction dir, int limit, CacheMode mode, RequestOptions? options) { if (dir == Direction.After && referenceMessageId == null) return AsyncEnumerable.Empty>(); IReadOnlyCollection cachedMessages = GetCachedMessages(channel, kook, messages, referenceMessageId, dir, limit); - IAsyncEnumerable> result = ImmutableArray.Create(cachedMessages) - .ToAsyncEnumerable>(); + IAsyncEnumerable> result = + ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); if (dir == Direction.Before) { limit -= cachedMessages.Count; - if (mode == CacheMode.CacheOnly || limit <= 0) return result; + if (mode == CacheMode.CacheOnly || limit <= 0) + return result; //Download remaining messages Guid? minId = cachedMessages.Count > 0 -#if NET6_0_OR_GREATER ? cachedMessages.MinBy(x => x.Timestamp)?.Id -#else - ? cachedMessages.OrderBy(x => x.Timestamp).FirstOrDefault()?.Id -#endif : referenceMessageId; IAsyncEnumerable> downloadedMessages = ChannelHelper.GetMessagesAsync(channel, kook, minId, dir, limit, true, options); - if (cachedMessages.Count != 0) - return result.Concat(downloadedMessages); - else - return downloadedMessages; + return cachedMessages.Count != 0 ? result.Concat(downloadedMessages) : downloadedMessages; } - else if (dir == Direction.After) + + if (dir == Direction.After) { if (mode == CacheMode.CacheOnly) return result; @@ -47,53 +42,39 @@ public static IAsyncEnumerable> GetMessagesAsync(I // 1. referenceMessageId is not null and corresponds to a message that is not in the cache, // so we have to make a request and ignore the cache if (referenceMessageId.HasValue && messages?.Get(referenceMessageId.Value) == null) - { ignoreCache = true; - } // 2. referenceMessageId is null or already in the cache, so we start from the cache else if (cachedMessages.Count > 0) { - referenceMessageId = cachedMessages -#if NET6_0_OR_GREATER - .MaxBy(x => x.Timestamp)?.Id; -#else - .OrderByDescending(x => x.Timestamp).FirstOrDefault()?.Id; -#endif + referenceMessageId = cachedMessages.MaxBy(x => x.Timestamp)?.Id; limit -= cachedMessages.Count; - if (limit <= 0) return result; } //Download remaining messages - var downloadedMessages = ChannelHelper.GetMessagesAsync( + IAsyncEnumerable> downloadedMessages = ChannelHelper.GetMessagesAsync( channel, kook, referenceMessageId, dir, limit, true, options); - if (!ignoreCache && cachedMessages.Count != 0) - return result.Concat(downloadedMessages); - else - return downloadedMessages; + return !ignoreCache && cachedMessages.Count != 0 + ? result.Concat(downloadedMessages) + : downloadedMessages; } - else //Direction.Around - { - if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count) return result; - //Cache isn't useful here since Kook will send them anyways - return ChannelHelper.GetMessagesAsync(channel, kook, referenceMessageId, dir, limit, true, options); - } - } + //Direction.Around + if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count) + return result; - public static IReadOnlyCollection GetCachedMessages(ISocketMessageChannel channel, KookSocketClient kook, MessageCache messages, - Guid? referenceMessageId, Direction dir, int limit) - { - if (messages != null) //Cache enabled - return messages.GetMany(referenceMessageId, dir, limit); - else - return ImmutableArray.Create(); + //Cache isn't useful here since Kook will send them anyway + return ChannelHelper.GetMessagesAsync(channel, kook, referenceMessageId, dir, limit, true, options); } + public static IReadOnlyCollection GetCachedMessages( + ISocketMessageChannel channel, KookSocketClient kook, MessageCache? messages, + Guid? referenceMessageId, Direction dir, int limit) => + messages?.GetMany(referenceMessageId, dir, limit) ?? []; + /// Unexpected type. - public static void AddMessage(ISocketMessageChannel channel, KookSocketClient kook, - SocketMessage msg) + public static void AddMessage(ISocketMessageChannel channel, KookSocketClient kook, SocketMessage msg) { switch (channel) { @@ -109,7 +90,7 @@ public static void AddMessage(ISocketMessageChannel channel, KookSocketClient ko } /// Unexpected type. - public static SocketMessage RemoveMessage(ISocketMessageChannel channel, KookSocketClient kook, Guid id) => + public static SocketMessage? RemoveMessage(ISocketMessageChannel channel, KookSocketClient kook, Guid id) => channel switch { SocketDMChannel dmChannel => dmChannel.RemoveMessage(id), @@ -117,22 +98,24 @@ public static SocketMessage RemoveMessage(ISocketMessageChannel channel, KookSoc _ => throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type.") }; - public static async Task UpdateAsync(SocketGuildChannel channel, RequestOptions options) + public static async Task UpdateAsync(SocketGuildChannel channel, RequestOptions? options) { Channel model = await channel.Kook.ApiClient.GetGuildChannelAsync(channel.Id, options).ConfigureAwait(false); channel.Update(channel.Kook.State, model); } - public static async Task UpdateAsync(SocketDMChannel channel, RequestOptions options) + public static async Task UpdateAsync(SocketDMChannel channel, RequestOptions? options) { UserChat model = await channel.Kook.ApiClient.GetUserChatAsync(channel.Id, options).ConfigureAwait(false); channel.Update(channel.Kook.State, model); } public static async Task> GetConnectedUsersAsync(SocketVoiceChannel channel, - SocketGuild guild, KookSocketClient kook, RequestOptions options) + SocketGuild guild, KookSocketClient kook, RequestOptions? options) { - IReadOnlyCollection users = await channel.Kook.ApiClient.GetConnectedUsersAsync(channel.Id, options).ConfigureAwait(false); - return users.Select(x => SocketGuildUser.Create(guild, kook.State, x)).ToImmutableArray(); + IReadOnlyCollection users = await channel.Kook.ApiClient + .GetConnectedUsersAsync(channel.Id, options) + .ConfigureAwait(false); + return [..users.Select(x => SocketGuildUser.Create(guild, kook.State, x))]; } } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 77bb33af..291519de 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -39,7 +39,7 @@ public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, public new IReadOnlyCollection Users => ImmutableArray.Create(Kook.CurrentUser, Recipient); internal SocketDMChannel(KookSocketClient kook, Guid chatCode, SocketUser recipient) - : base(kook, default(ulong)) + : base(kook, default) { Id = chatCode; Recipient = recipient; @@ -61,20 +61,19 @@ internal void Update(ClientState state, User recipient) internal void Update(ClientState state, UserChat model) => Recipient.Update(state, model.Recipient); /// - public override Task UpdateAsync(RequestOptions options = null) - => SocketChannelHelper.UpdateAsync(this, options); + public override Task UpdateAsync(RequestOptions? options = null) => + SocketChannelHelper.UpdateAsync(this, options); /// - public Task CloseAsync(RequestOptions options = null) - => ChannelHelper.DeleteDMChannelAsync(this, Kook, options); + public Task CloseAsync(RequestOptions? options = null) => + ChannelHelper.DeleteDMChannelAsync(this, Kook, options); #endregion #region Messages /// - public SocketMessage GetCachedMessage(Guid id) - => null; + public SocketMessage? GetCachedMessage(Guid id) => null; /// /// Gets the message associated with the given . @@ -84,7 +83,7 @@ public SocketMessage GetCachedMessage(Guid id) /// /// The message gotten from either the cache or the download, or null if none is found. /// - public async Task GetMessageAsync(Guid id, RequestOptions options = null) => + public async Task GetMessageAsync(Guid id, RequestOptions? options = null) => await ChannelHelper.GetDirectMessageAsync(this, Kook, id, options).ConfigureAwait(false); /// @@ -99,8 +98,9 @@ public async Task GetMessageAsync(Guid id, RequestOptions options = nu /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); + public IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, null, Direction.Before, limit, true, options); /// /// Gets a collection of messages in this channel. @@ -116,9 +116,9 @@ public IAsyncEnumerable> GetMessagesAsync(int limi /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); + public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessageId, dir, limit, true, options); /// /// Gets a collection of messages in this channel. @@ -134,21 +134,20 @@ public IAsyncEnumerable> GetMessagesAsync(Guid ref /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + ChannelHelper.GetDirectMessagesAsync(this, Kook, referenceMessage.Id, dir, limit, true, options); /// - public IReadOnlyCollection GetCachedMessages(int limit = KookConfig.MaxMessagesPerBatch) - => ImmutableArray.Create(); + public IReadOnlyCollection GetCachedMessages(int limit = KookConfig.MaxMessagesPerBatch) => []; /// - public IReadOnlyCollection GetCachedMessages(Guid fromMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch) - => ImmutableArray.Create(); + public IReadOnlyCollection GetCachedMessages(Guid fromMessageId, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch) => []; /// - public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch) - => ImmutableArray.Create(); + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch) => []; /// /// Sends a file to this message channel. @@ -157,7 +156,7 @@ public IReadOnlyCollection GetCachedMessages(IMessage fromMessage /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The file path of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. @@ -165,9 +164,9 @@ public IReadOnlyCollection GetCachedMessages(IMessage fromMessage /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, path, fileName, type, options, quote); + public Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, path, filename, type, quote, options); /// /// Sends a file to this message channel. @@ -176,7 +175,7 @@ public Task> SendFileAsync(string path, string fil /// This method sends a file as if you are uploading a file directly from your Kook client. /// /// The stream of the file. - /// The name of the file. + /// The name of the file. /// The type of the file. /// The message quote to be included. Used to reply to specific messages. /// The options to be used when sending the request. @@ -184,9 +183,9 @@ public Task> SendFileAsync(string path, string fil /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendFileAsync(Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, stream, fileName, type, options, quote); + public Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, stream, filename, type, quote, options); /// /// Sends a file to this message channel. @@ -201,8 +200,9 @@ public Task> SendFileAsync(Stream stream, string f /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendFileAsync(FileAttachment attachment, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectFileAsync(this, Kook, attachment, options, quote); + public Task> SendFileAsync(FileAttachment attachment, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectFileAsync(this, Kook, attachment, quote, options); /// /// Sends a text message to this message channel. @@ -214,8 +214,9 @@ public Task> SendFileAsync(FileAttachment attachme /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendTextAsync(string text, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectMessageAsync(this, Kook, MessageType.KMarkdown, text, options, quote); + public Task> SendTextAsync(string text, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectMessageAsync(this, Kook, MessageType.KMarkdown, text, quote, options); /// /// Sends a card message to this message channel. @@ -227,8 +228,9 @@ public Task> SendTextAsync(string text, IQuote quo /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectCardsAsync(this, Kook, cards, options, quote); + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectCardsAsync(this, Kook, cards, quote, options); /// /// Sends a card message to this message channel. @@ -240,27 +242,28 @@ public Task> SendCardsAsync(IEnumerable car /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the identifier and timestamp of the sent message. /// - public Task> SendCardAsync(ICard card, IQuote quote = null, RequestOptions options = null) - => ChannelHelper.SendDirectCardAsync(this, Kook, card, options, quote); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, RequestOptions? options = null) => + ChannelHelper.SendDirectCardAsync(this, Kook, card, quote, options); /// - public async Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) - => await ChannelHelper.ModifyDirectMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); + public async Task ModifyMessageAsync(Guid messageId, + Action func, RequestOptions? options = null) => + await ChannelHelper.ModifyDirectMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) - => ChannelHelper.DeleteDirectMessageAsync(this, messageId, Kook, options); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + ChannelHelper.DeleteDirectMessageAsync(this, messageId, Kook, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteDirectMessageAsync(this, message.Id, Kook, options); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + ChannelHelper.DeleteDirectMessageAsync(this, message.Id, Kook, options); internal void AddMessage(SocketMessage msg) { } - internal SocketMessage RemoveMessage(Guid id) - => null; + internal SocketMessage? RemoveMessage(Guid id) => null; #endregion @@ -273,14 +276,10 @@ internal SocketMessage RemoveMessage(Guid id) /// /// A object that is a recipient of this channel; otherwise null. /// - public new SocketUser GetUser(ulong id) + public new SocketUser? GetUser(ulong id) { - if (id == Recipient.Id) - return Recipient; - else if (id == Kook.CurrentUser.Id) - return Kook.CurrentUser; - else - return null; + if (id == Recipient.Id) return Recipient; + return id == Kook.CurrentUser?.Id ? Kook.CurrentUser : null; } #endregion @@ -295,7 +294,7 @@ internal override void Update(ClientState state, Channel model) => internal override IReadOnlyCollection GetUsersInternal() => Users; /// - internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + internal override SocketUser? GetUserInternal(ulong id) => GetUser(id); #endregion @@ -309,101 +308,105 @@ internal override void Update(ClientState state, Channel model) => #region ISocketPrivateChannel /// - IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + IReadOnlyCollection ISocketPrivateChannel.Recipients => [Recipient]; #endregion #region IPrivateChannel /// - IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + IReadOnlyCollection IPrivateChannel.Recipients => [Recipient]; #endregion #region IMessageChannel /// - Task> IDMChannel.SendFileAsync(string path, string fileName, - AttachmentType type, IQuote quote, RequestOptions options) - => SendFileAsync(path, fileName, type, (Quote)quote, options); + Task> IDMChannel.SendFileAsync(string path, string? filename, + AttachmentType type, IQuote? quote, RequestOptions? options) => + SendFileAsync(path, filename, type, quote, options); /// - Task> IDMChannel.SendFileAsync(Stream stream, string fileName, - AttachmentType type, IQuote quote, RequestOptions options) - => SendFileAsync(stream, fileName, type, (Quote)quote, options); + Task> IDMChannel.SendFileAsync(Stream stream, string filename, + AttachmentType type, IQuote? quote, RequestOptions? options) => + SendFileAsync(stream, filename, type, quote, options); /// Task> IDMChannel.SendFileAsync(FileAttachment attachment, - IQuote quote, RequestOptions options) - => SendFileAsync(attachment, (Quote)quote, options); + IQuote? quote, RequestOptions? options) => + SendFileAsync(attachment, quote, options); /// Task> IDMChannel.SendTextAsync(string text, - IQuote quote, RequestOptions options) - => SendTextAsync(text, (Quote)quote, options); + IQuote? quote, RequestOptions? options) => + SendTextAsync(text, quote, options); /// Task> IDMChannel.SendCardAsync(ICard card, - IQuote quote, RequestOptions options) - => SendCardAsync(card, (Quote)quote, options); + IQuote? quote, RequestOptions? options) => + SendCardAsync(card, quote, options); /// Task> IDMChannel.SendCardsAsync(IEnumerable cards, - IQuote quote, RequestOptions options) - => SendCardsAsync(cards, (Quote)quote, options); + IQuote? quote, RequestOptions? options) => + SendCardsAsync(cards, quote, options); /// - async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return GetCachedMessage(id); - } + async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetMessageAsync(id, options).ConfigureAwait(false) + : GetCachedMessage(id); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(limit, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, + CacheMode mode, RequestOptions? options) => + mode == CacheMode.CacheOnly + ? ImmutableArray>.Empty.ToAsyncEnumerable() + : GetMessagesAsync(limit, options); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit, - CacheMode mode, RequestOptions options) - => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(referenceMessageId, dir, limit, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.CacheOnly + ? ImmutableArray>.Empty.ToAsyncEnumerable() + : GetMessagesAsync(referenceMessageId, dir, limit, options); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit, - CacheMode mode, RequestOptions options) - => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(referenceMessage.Id, dir, limit, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.CacheOnly + ? ImmutableArray>.Empty.ToAsyncEnumerable() + : GetMessagesAsync(referenceMessage.Id, dir, limit, options); /// - Task> IMessageChannel.SendFileAsync(string path, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(path, fileName, type, quote, options); + Task> IMessageChannel.SendFileAsync(string path, string? filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(path, filename, type, quote, options); /// - Task> IMessageChannel.SendFileAsync(Stream stream, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(stream, fileName, type, quote, options); + Task> IMessageChannel.SendFileAsync(Stream stream, string filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(stream, filename, type, quote, options); /// Task> IMessageChannel.SendFileAsync(FileAttachment attachment, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(attachment, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(attachment, quote, options); /// Task> IMessageChannel.SendTextAsync(string text, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendTextAsync(text, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendTextAsync(text, quote, options); /// Task> IMessageChannel.SendCardsAsync(IEnumerable cards, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardsAsync(cards, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardsAsync(cards, quote, options); /// Task> IMessageChannel.SendCardAsync(ICard card, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardAsync(card, quote, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardAsync(card, quote, options); #endregion @@ -413,12 +416,12 @@ Task> IMessageChannel.SendCardAsync(ICard card, string IChannel.Name => $"@{Recipient}"; /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + ImmutableArray.Create>(Users).ToAsyncEnumerable(); #endregion @@ -428,5 +431,5 @@ IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mo public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; + internal new SocketDMChannel Clone() => (SocketDMChannel)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 79ebcb6e..0c80dc09 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -8,7 +8,7 @@ namespace Kook.WebSocket; /// /// Represent a WebSocket-based guild channel. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketGuildChannel : SocketChannel, IGuildChannel { private ImmutableArray _rolePermissionOverwrites; @@ -47,7 +47,7 @@ public class SocketGuildChannel : SocketChannel, IGuildChannel /// /// A task that represents the asynchronous get operation. The task result contains the creator of this channel. /// - public SocketGuildUser Creator => GetUser(CreatorId); + public SocketGuildUser? Creator => GetUser(CreatorId); /// public IReadOnlyCollection RolePermissionOverwrites => _rolePermissionOverwrites; @@ -61,13 +61,16 @@ public class SocketGuildChannel : SocketChannel, IGuildChannel /// /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). /// - public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); + public new virtual IReadOnlyCollection Users => []; internal SocketGuildChannel(KookSocketClient kook, ulong id, SocketGuild guild) : base(kook, id) { + Name = string.Empty; Guild = guild; Type = ChannelType.Unspecified; + _rolePermissionOverwrites = []; + _userPermissionOverwrites = []; } internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) => @@ -86,54 +89,31 @@ internal override void Update(ClientState state, Model model) Position = model.Position; CreatorId = model.CreatorId; - API.RolePermissionOverwrite[] rolePermissionOverwrites = model.RolePermissionOverwrites; - ImmutableArray.Builder newRoleOverwrites = - ImmutableArray.CreateBuilder(rolePermissionOverwrites.Length); - foreach (API.RolePermissionOverwrite x in rolePermissionOverwrites) - newRoleOverwrites.Add(x.ToEntity()); - - _rolePermissionOverwrites = newRoleOverwrites.ToImmutable(); - - API.UserPermissionOverwrite[] userPermissionOverwrites = model.UserPermissionOverwrites; - ImmutableArray.Builder newUserOverwrites = - ImmutableArray.CreateBuilder(userPermissionOverwrites.Length); - foreach (API.UserPermissionOverwrite x in userPermissionOverwrites) - newUserOverwrites.Add(x.ToEntity(Kook, state)); - - _userPermissionOverwrites = newUserOverwrites.ToImmutable(); + _rolePermissionOverwrites = [..model.RolePermissionOverwrites.Select(x => x.ToEntity())]; + _userPermissionOverwrites = [..model.UserPermissionOverwrites.Select(x => x.ToEntity(Kook, state))]; } internal void RemoveRolePermissionOverwrite(uint roleId) { - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == roleId) - { - _rolePermissionOverwrites = _rolePermissionOverwrites.RemoveAt(i); - return; - } + _rolePermissionOverwrites = [.._rolePermissionOverwrites.Where(x => x.Target != roleId)]; } internal void RemoveUserPermissionOverwrite(ulong userId) { - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == userId) - { - _userPermissionOverwrites = _userPermissionOverwrites.RemoveAt(i); - return; - } + _userPermissionOverwrites = [.._userPermissionOverwrites.Where(x => x.Target.Id != userId)]; } /// - public override Task UpdateAsync(RequestOptions options = null) - => SocketChannelHelper.UpdateAsync(this, options); + public override Task UpdateAsync(RequestOptions? options = null) => + SocketChannelHelper.UpdateAsync(this, options); /// - public Task ModifyAsync(Action func, RequestOptions options = null) - => ChannelHelper.ModifyAsync(this, Kook, func, options); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + ChannelHelper.ModifyAsync(this, Kook, func, options); /// - public Task DeleteAsync(RequestOptions options = null) - => ChannelHelper.DeleteGuildChannelAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + ChannelHelper.DeleteGuildChannelAsync(this, Kook, options); /// /// Gets the permission overwrite for a specific user. @@ -142,14 +122,8 @@ public Task DeleteAsync(RequestOptions options = null) /// /// An overwrite object for the targeted user; null if none is set. /// - public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) - { - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - return _userPermissionOverwrites[i].Permissions; - - return null; - } + public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) => + _userPermissionOverwrites.FirstOrDefault(x => x.Target.Id == user.Id)?.Permissions; /// /// Gets the permission overwrite for a specific role. @@ -158,14 +132,8 @@ public Task DeleteAsync(RequestOptions options = null) /// /// An overwrite object for the targeted role; null if none is set. /// - public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) - { - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - return _rolePermissionOverwrites[i].Permissions; - - return null; - } + public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) => + _rolePermissionOverwrites.FirstOrDefault(x => x.Target == role.Id)?.Permissions; /// /// Adds or updates the permission overwrite for the given user. @@ -175,10 +143,12 @@ public Task DeleteAsync(RequestOptions options = null) /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) + public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) { - UserPermissionOverwrite perms = await ChannelHelper.AddPermissionOverwriteAsync(this, Kook, user, options).ConfigureAwait(false); - _userPermissionOverwrites = _userPermissionOverwrites.Add(perms); + UserPermissionOverwrite perms = await ChannelHelper + .AddPermissionOverwriteAsync(this, Kook, user, options) + .ConfigureAwait(false); + _userPermissionOverwrites = [.._userPermissionOverwrites, perms]; } /// @@ -189,10 +159,12 @@ public async Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions op /// /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. /// - public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null) + public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null) { - RolePermissionOverwrite perms = await ChannelHelper.AddPermissionOverwriteAsync(this, Kook, role, options).ConfigureAwait(false); - _rolePermissionOverwrites = _rolePermissionOverwrites.Add(perms); + RolePermissionOverwrite perms = await ChannelHelper + .AddPermissionOverwriteAsync(this, Kook, role, options) + .ConfigureAwait(false); + _rolePermissionOverwrites = [.._rolePermissionOverwrites, perms]; } /// @@ -203,16 +175,10 @@ public async Task AddPermissionOverwriteAsync(IRole role, RequestOptions options /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) + public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Kook, user, options).ConfigureAwait(false); - - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - { - _userPermissionOverwrites = _userPermissionOverwrites.RemoveAt(i); - return; - } + RemoveUserPermissionOverwrite(user.Id); } /// @@ -223,16 +189,10 @@ public async Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Kook, role, options).ConfigureAwait(false); - - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - { - _rolePermissionOverwrites = _rolePermissionOverwrites.RemoveAt(i); - return; - } + RemoveRolePermissionOverwrite(role.Id); } /// @@ -244,17 +204,13 @@ public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions opti /// /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, RequestOptions options = null) + public async Task ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options = null) { - UserPermissionOverwrite perms = await ChannelHelper.ModifyPermissionOverwriteAsync(this, Kook, user, func, options).ConfigureAwait(false); - - for (int i = 0; i < _userPermissionOverwrites.Length; i++) - if (_userPermissionOverwrites[i].Target.Id == user.Id) - { - _userPermissionOverwrites = _userPermissionOverwrites.RemoveAt(i); - _userPermissionOverwrites = _userPermissionOverwrites.Add(perms); - return; - } + UserPermissionOverwrite perms = await ChannelHelper + .ModifyPermissionOverwriteAsync(this, Kook, user, func, options) + .ConfigureAwait(false); + _userPermissionOverwrites = [.._userPermissionOverwrites.Where(x => x.Target.Id != user.Id), perms]; } /// @@ -266,17 +222,13 @@ public async Task ModifyPermissionOverwriteAsync(IGuildUser user, Func /// A task representing the asynchronous operation for removing the specified permissions from the channel. /// - public async Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null) + public async Task ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options = null) { - RolePermissionOverwrite perms = await ChannelHelper.ModifyPermissionOverwriteAsync(this, Kook, role, func, options).ConfigureAwait(false); - - for (int i = 0; i < _rolePermissionOverwrites.Length; i++) - if (_rolePermissionOverwrites[i].Target == role.Id) - { - _rolePermissionOverwrites = _rolePermissionOverwrites.RemoveAt(i); - _rolePermissionOverwrites = _rolePermissionOverwrites.Add(perms); - return; - } + RolePermissionOverwrite perms = await ChannelHelper + .ModifyPermissionOverwriteAsync(this, Kook, role, func, options) + .ConfigureAwait(false); + _rolePermissionOverwrites = [.._rolePermissionOverwrites.Where(x => x.Target != role.Id), perms]; } /// @@ -284,7 +236,7 @@ public async Task ModifyPermissionOverwriteAsync(IRole role, Func /// The user's identifier. /// A with the provided identifier; null if none is found. - public new virtual SocketGuildUser GetUser(ulong id) => null; + public new virtual SocketGuildUser? GetUser(ulong id) => null; /// /// Gets the name of the channel. @@ -295,14 +247,14 @@ public async Task ModifyPermissionOverwriteAsync(IRole role, Func Name; private string DebuggerDisplay => $"{Name} ({Id}, Guild)"; - internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; + internal new SocketGuildChannel Clone() => (SocketGuildChannel)MemberwiseClone(); #endregion #region SocketChannel /// - internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + internal override SocketUser? GetUserInternal(ulong id) => GetUser(id); /// internal override IReadOnlyCollection GetUsersInternal() => Users; @@ -318,37 +270,39 @@ public async Task ModifyPermissionOverwriteAsync(IRole role, Func Guild.Id; /// - async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, RequestOptions options) - => await AddPermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, RequestOptions? options) => + await AddPermissionOverwriteAsync(role, options).ConfigureAwait(false); /// - async Task IGuildChannel.AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options) - => await AddPermissionOverwriteAsync(user, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options) => + await AddPermissionOverwriteAsync(user, options).ConfigureAwait(false); /// - async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) - => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions? options) => + await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); /// - async Task IGuildChannel.RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options) - => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options) => + await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); /// - async Task IGuildChannel.ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options) - => await ModifyPermissionOverwriteAsync(role, func, options).ConfigureAwait(false); + async Task IGuildChannel.ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options) => + await ModifyPermissionOverwriteAsync(role, func, options).ConfigureAwait(false); /// - async Task IGuildChannel.ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options) - => await ModifyPermissionOverwriteAsync(user, func, options).ConfigureAwait(false); + async Task IGuildChannel.ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options) => + await ModifyPermissionOverwriteAsync(user, func, options).ConfigureAwait(false); /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => + ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); //Overridden in Text/Voice + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); //Overridden in Text/Voice /// /// Gets the creator of this channel. @@ -361,20 +315,20 @@ Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOpt /// /// A task that represents the asynchronous get operation. The task result contains the creator of this channel. /// - Task IGuildChannel.GetCreatorAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(Creator); + Task IGuildChannel.GetCreatorAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(Creator); #endregion #region IChannel /// - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); //Overridden in Text/Voice + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); //Overridden in Text/Voice #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index d131e429..acd58cd8 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -8,12 +8,12 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based channel in a guild that can send and receive messages. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel { #region SocketTextChannel - private readonly MessageCache _messages; + private readonly MessageCache? _messages; /// public string Topic { get; private set; } @@ -30,8 +30,9 @@ public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessag /// /// An representing the parent of this channel; null if none is set. /// - public ICategoryChannel Category - => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + public ICategoryChannel? Category => CategoryId.HasValue + ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel + : null; /// public bool? IsPermissionSynced { get; private set; } @@ -47,18 +48,20 @@ public ICategoryChannel Category /// This property is only available if the is set to a value greater than zero. /// /// - public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? []; /// - public override IReadOnlyCollection Users - => Guild.Users.Where(x => Permissions.GetValue( + public override IReadOnlyCollection Users => Guild.Users + .Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), - ChannelPermission.ViewChannel)).ToImmutableArray(); + ChannelPermission.ViewChannel)) + .ToImmutableArray(); internal SocketTextChannel(KookSocketClient kook, ulong id, SocketGuild guild) : base(kook, id, guild) { Type = ChannelType.Text; + Topic = string.Empty; if (Kook.MessageCacheSize > 0) _messages = new MessageCache(Kook); } @@ -74,29 +77,28 @@ internal override void Update(ClientState state, Model model) { base.Update(state, model); CategoryId = model.CategoryId != 0 ? model.CategoryId : null; - Topic = model.Topic; + Topic = model.Topic ?? string.Empty; SlowModeInterval = model.SlowMode / 1000; IsPermissionSynced = model.PermissionSync; } /// - public virtual Task ModifyAsync(Action func, RequestOptions options = null) - => ChannelHelper.ModifyAsync(this, Kook, func, options); + public virtual Task ModifyAsync(Action func, RequestOptions? options = null) => + ChannelHelper.ModifyAsync(this, Kook, func, options); /// - public virtual Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Kook, options); + public virtual Task SyncPermissionsAsync(RequestOptions? options = null) => + ChannelHelper.SyncPermissionsAsync(this, Kook, options); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + internal new SocketTextChannel Clone() => (SocketTextChannel)MemberwiseClone(); #endregion #region Messages /// - public SocketMessage GetCachedMessage(Guid id) - => _messages?.Get(id); + public SocketMessage? GetCachedMessage(Guid id) => _messages?.Get(id); /// /// Gets a message from this message channel. @@ -111,19 +113,18 @@ public SocketMessage GetCachedMessage(Guid id) /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - public async Task GetMessageAsync(Guid id, RequestOptions options = null) + public async Task GetMessageAsync(Guid id, RequestOptions? options = null) { - IMessage msg = _messages?.Get(id); - if (msg == null) msg = await ChannelHelper.GetMessageAsync(this, Kook, id, options).ConfigureAwait(false); - - return msg; + IMessage? msg = _messages?.Get(id); + return msg ?? await ChannelHelper.GetMessageAsync(this, Kook, id, options).ConfigureAwait(false); } /// /// Gets the last N messages from this message channel. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in + /// . /// Please visit its documentation for more details on this method. /// /// The numbers of message to be gotten from. @@ -131,14 +132,17 @@ public async Task GetMessageAsync(Guid id, RequestOptions options = nu /// /// Paged collection of messages. /// - public virtual IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + public virtual IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, + null, Direction.Before, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in + /// . /// Please visit its documentation for more details on this method. /// /// The ID of the starting message to get the messages from. @@ -148,15 +152,17 @@ public virtual IAsyncEnumerable> GetMessagesAsync( /// /// Paged collection of messages. /// - public virtual IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessageId, dir, limit, CacheMode.AllowDownload, options); + public virtual IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, + referenceMessageId, dir, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in + /// . /// Please visit its documentation for more details on this method. /// /// The starting message to get the messages from. @@ -166,107 +172,110 @@ public virtual IAsyncEnumerable> GetMessagesAsync( /// /// Paged collection of messages. /// - public virtual IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessage.Id, dir, limit, CacheMode.AllowDownload, options); + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, + referenceMessage.Id, dir, limit, CacheMode.AllowDownload, options); /// - public IReadOnlyCollection GetCachedMessages(int limit = KookConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Kook, _messages, null, Direction.Before, limit); + public IReadOnlyCollection GetCachedMessages(int limit = KookConfig.MaxMessagesPerBatch) => + SocketChannelHelper.GetCachedMessages(this, Kook, _messages, null, Direction.Before, limit); /// - public IReadOnlyCollection GetCachedMessages(Guid fromMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Kook, _messages, fromMessageId, dir, limit); + public IReadOnlyCollection GetCachedMessages(Guid fromMessageId, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch) => + SocketChannelHelper.GetCachedMessages(this, Kook, _messages, fromMessageId, dir, limit); /// - public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Kook, _messages, fromMessage.Id, dir, limit); + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch) => + SocketChannelHelper.GetCachedMessages(this, Kook, _messages, fromMessage.Id, dir, limit); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Kook, options); + public Task> GetPinnedMessagesAsync(RequestOptions? options = null) => + ChannelHelper.GetPinnedMessagesAsync(this, Kook, options); /// - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - Quote quote = null, IUser ephemeralUser = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, path, fileName, type, options, quote, ephemeralUser); + public Task> SendFileAsync(string path, string? filename = null, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) + { + string name = filename ?? Path.GetFileName(path); + return ChannelHelper.SendFileAsync(this, Kook, path, name, type, quote, ephemeralUser, options); + } /// - public Task> SendFileAsync(Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, - Quote quote = null, IUser ephemeralUser = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, stream, fileName, type, options, quote, ephemeralUser); + public Task> SendFileAsync(Stream stream, string filename, + AttachmentType type = AttachmentType.File, IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) => + ChannelHelper.SendFileAsync(this, Kook, stream, filename, type, quote, ephemeralUser, options); /// - public Task> SendFileAsync(FileAttachment attachment, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Kook, attachment, options, quote, ephemeralUser); + public Task> SendFileAsync(FileAttachment attachment, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendFileAsync(this, Kook, attachment, quote, ephemeralUser, options); /// - public Task> SendTextAsync(string text, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Kook, MessageType.KMarkdown, text, options, quote, ephemeralUser); + public Task> SendTextAsync(string text, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendMessageAsync(this, Kook, MessageType.KMarkdown, text, quote, ephemeralUser, options); /// - public Task> SendCardsAsync(IEnumerable cards, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendCardsAsync(this, Kook, cards, options, quote, ephemeralUser); + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendCardsAsync(this, Kook, cards, quote, ephemeralUser, options); /// - public Task> SendCardAsync(ICard card, Quote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) - => ChannelHelper.SendCardAsync(this, Kook, card, options, quote, ephemeralUser); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + ChannelHelper.SendCardAsync(this, Kook, card, quote, ephemeralUser, options); /// - public async Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) - => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); + public async Task ModifyMessageAsync(Guid messageId, + Action func, RequestOptions? options = null) => + await ChannelHelper.ModifyMessageAsync(this, messageId, func, Kook, options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, messageId, Kook, options); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + ChannelHelper.DeleteMessageAsync(this, messageId, Kook, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, message.Id, Kook, options); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + ChannelHelper.DeleteMessageAsync(this, message.Id, Kook, options); - internal void AddMessage(SocketMessage msg) - => _messages?.Add(msg); + internal void AddMessage(SocketMessage msg) => _messages?.Add(msg); - internal SocketMessage RemoveMessage(Guid id) - => _messages?.Remove(id); + internal SocketMessage? RemoveMessage(Guid id) => _messages?.Remove(id); #endregion #region Invites /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); + public async Task> GetInvitesAsync(RequestOptions? options = null) => + await ChannelHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 604800, + int? maxUses = null, RequestOptions? options = null) => + await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => + await ChannelHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); #endregion #region Users /// - public override SocketGuildUser GetUser(ulong id) + public override SocketGuildUser? GetUser(ulong id) { - SocketGuildUser user = Guild.GetUser(id); - if (user != null) - { - ulong guildPerms = Permissions.ResolveGuild(Guild, user); - ulong channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); - if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) return user; - } - - return null; + if (Guild.GetUser(id) is not { } user) return null; + ulong guildPerms = Permissions.ResolveGuild(Guild, user); + ulong channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + return Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel) ? user : null; } #endregion @@ -274,16 +283,18 @@ public override SocketGuildUser GetUser(ulong id) #region IGuildChannel /// - async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { - SocketGuildUser user = GetUser(id); - if (user is not null || mode == CacheMode.CacheOnly) return user; - + if (GetUser(id) is { } user) + return user; + if (mode == CacheMode.CacheOnly) + return null; return await ChannelHelper.GetUserAsync(this, Guild, Kook, id, options).ConfigureAwait(false); } /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => mode == CacheMode.AllowDownload ? ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options) : ImmutableArray.Create>(Users).ToAsyncEnumerable(); @@ -293,69 +304,67 @@ IAsyncEnumerable> IGuildChannel.GetUsersAsync(Ca #region IMessageChannel /// - Task> IMessageChannel.SendFileAsync(string path, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(path, fileName, type, (Quote)quote, ephemeralUser, options); + Task> IMessageChannel.SendFileAsync(string path, string? filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(path, filename, type, quote, ephemeralUser, options); /// - Task> IMessageChannel.SendFileAsync(Stream stream, string fileName, - AttachmentType type, IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(stream, fileName, type, (Quote)quote, ephemeralUser, options); + Task> IMessageChannel.SendFileAsync(Stream stream, string filename, + AttachmentType type, IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(stream, filename, type, quote, ephemeralUser, options); /// Task> IMessageChannel.SendFileAsync(FileAttachment attachment, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendFileAsync(attachment, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendFileAsync(attachment, quote, ephemeralUser, options); /// Task> IMessageChannel.SendTextAsync(string text, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendTextAsync(text, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendTextAsync(text, quote, ephemeralUser, options); /// Task> IMessageChannel.SendCardAsync(ICard card, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardAsync(card, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardAsync(card, quote, ephemeralUser, options); /// Task> IMessageChannel.SendCardsAsync(IEnumerable cards, - IQuote quote, IUser ephemeralUser, RequestOptions options) - => SendCardsAsync(cards, (Quote)quote, ephemeralUser, options); + IQuote? quote, IUser? ephemeralUser, RequestOptions? options) => + SendCardsAsync(cards, quote, ephemeralUser, options); /// - async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return GetCachedMessage(id); - } + async Task IMessageChannel.GetMessageAsync(Guid id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetMessageAsync(id, options).ConfigureAwait(false) + : GetCachedMessage(id); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, null, Direction.Before, limit, mode, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync( + int limit, CacheMode mode, RequestOptions? options) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, null, Direction.Before, limit, mode, options); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit, - CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessageId, dir, limit, mode, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(Guid referenceMessageId, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessageId, dir, limit, mode, options); /// - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit, - CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessage.Id, dir, limit, mode, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage referenceMessage, + Direction dir, int limit, CacheMode mode, RequestOptions? options) => + SocketChannelHelper.GetMessagesAsync(this, Kook, _messages, referenceMessage.Id, dir, limit, mode, options); /// - async Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + async Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions? options) => + await GetPinnedMessagesAsync(options).ConfigureAwait(false); #endregion #region INestedChannel /// - Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(Category); + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(Category); #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Kook.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index f5d53637..7bd7fb7b 100644 --- a/src/Kook.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Kook.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -9,7 +9,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based voice channel in a guild. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketVoiceChannel : SocketTextChannel, IVoiceChannel, ISocketAudioChannel { #region SocketVoiceChannel @@ -18,24 +18,24 @@ public class SocketVoiceChannel : SocketTextChannel, IVoiceChannel, ISocketAudio public VoiceQuality? VoiceQuality { get; private set; } /// - public int? UserLimit { get; private set; } + public int UserLimit { get; private set; } /// - public string ServerUrl { get; private set; } + public string? ServerUrl { get; private set; } /// public bool? IsVoiceRegionOverwritten { get; private set; } /// - public string VoiceRegion { get; private set; } + public string? VoiceRegion { get; private set; } /// public bool HasPassword { get; private set; } /// /// - public override IReadOnlyCollection Users - => Guild.Users.Where(x => Permissions.GetValue( + public override IReadOnlyCollection Users => + Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), ChannelPermission.ViewChannel)).ToImmutableArray(); @@ -55,8 +55,8 @@ public override IReadOnlyCollection Users /// /// A read-only collection of users that are currently connected to this voice channel. /// - public IReadOnlyCollection ConnectedUsers - => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + public IReadOnlyCollection ConnectedUsers => + Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); internal SocketVoiceChannel(KookSocketClient kook, ulong id, SocketGuild guild) : base(kook, id, guild) @@ -76,7 +76,7 @@ internal override void Update(ClientState state, Model model) { base.Update(state, model); VoiceQuality = model.VoiceQuality; - UserLimit = model.UserLimit ?? 0; + UserLimit = model.UserLimit; ServerUrl = model.ServerUrl; VoiceRegion = model.VoiceRegion; HasPassword = model.HasPassword; @@ -84,17 +84,15 @@ internal override void Update(ClientState state, Model model) } /// - public override SocketGuildUser GetUser(ulong id) + public override SocketGuildUser? GetUser(ulong id) { - SocketGuildUser user = Guild.GetUser(id); - if (user?.VoiceChannel?.Id == Id) - return user; - return null; + SocketGuildUser? user = Guild.GetUser(id); + return user?.VoiceChannel?.Id == Id ? user : null; } /// - public Task ModifyAsync(Action func, RequestOptions options = null) - => ChannelHelper.ModifyAsync(this, Kook, func, options); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + ChannelHelper.ModifyAsync(this, Kook, func, options); /// /// Gets a collection of users that are currently connected to this voice channel. @@ -105,14 +103,11 @@ public Task ModifyAsync(Action func, RequestOption /// A task that represents the asynchronous get operation. The task result contains a read-only collection of users /// that are currently connected to this voice channel. /// - public async Task> GetConnectedUsersAsync(CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) - { - if (mode is CacheMode.AllowDownload) - return await SocketChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false); - else - return ConnectedUsers; - } + public async Task> GetConnectedUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => + mode is CacheMode.AllowDownload + ? await SocketChannelHelper.GetConnectedUsersAsync(this, Guild, Kook, options).ConfigureAwait(false) + : ConnectedUsers; #endregion @@ -121,25 +116,25 @@ public async Task> GetConnectedUsersAsync(C /// /// Getting messages from a voice channel is not supported. public override IAsyncEnumerable> GetMessagesAsync( - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. public override IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. public override IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, RequestOptions options = null) - => throw new NotSupportedException("Getting messages from a voice channel is not supported."); + int limit = KookConfig.MaxMessagesPerBatch, RequestOptions? options = null) => + throw new NotSupportedException("Getting messages from a voice channel is not supported."); /// /// Getting messages from a voice channel is not supported. - Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions options) - => Task.FromException>( + Task> ITextChannel.GetPinnedMessagesAsync(RequestOptions? options) => + Task.FromException>( new NotSupportedException("Getting messages from a voice channel is not supported.")); #endregion @@ -147,35 +142,37 @@ Task> ITextChannel.GetPinnedMessagesAsync(RequestO #region IVoiceChannel /// - async Task> IVoiceChannel.GetConnectedUsersAsync(CacheMode mode, RequestOptions options) - => await GetConnectedUsersAsync(mode, options).ConfigureAwait(false); + async Task> IVoiceChannel.GetConnectedUsersAsync( + CacheMode mode, RequestOptions? options) => + await GetConnectedUsersAsync(mode, options).ConfigureAwait(false); #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + internal new SocketVoiceChannel Clone() => (SocketVoiceChannel)MemberwiseClone(); #region IAudioChannel /// - public Task ConnectAsync(/*bool selfDeaf = false, bool selfMute = false, */bool external = false, bool disconnect = true) - => Guild.ConnectAudioAsync(Id, /*selfDeaf, selfMute, */external, disconnect); + public Task ConnectAsync( /*bool selfDeaf = false, bool selfMute = false, */ + bool external = false, bool disconnect = true) => + Guild.ConnectAudioAsync(Id, /*selfDeaf, selfMute, */external, disconnect); /// - public Task DisconnectAsync() - => Guild.DisconnectAudioAsync(); + public Task DisconnectAsync() => Guild.DisconnectAudioAsync(); #endregion #region IGuildChannel /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); /// /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + IAsyncEnumerable> IGuildChannel.GetUsersAsync( + CacheMode mode, RequestOptions? options) => mode == CacheMode.AllowDownload ? ChannelHelper.GetUsersAsync(this, Guild, Kook, KookConfig.MaxUsersPerBatch, 1, options) : ImmutableArray.Create>(Users).ToAsyncEnumerable(); @@ -185,8 +182,8 @@ IAsyncEnumerable> IGuildChannel.GetUsersAsync(Ca #region INestedChannel /// - Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(Category); + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(Category); #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 47f02bcb..5145c49f 100644 --- a/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -19,21 +19,21 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based guild object. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable { #region SocketGuild - private ConcurrentDictionary _channels; - private ConcurrentDictionary _members; - private ConcurrentDictionary _roles; - private ConcurrentDictionary _voiceStates; - private ConcurrentDictionary _emotes; - private Dictionary> _boostSubscriptions; + private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _members; + private readonly ConcurrentDictionary _roles; + private readonly ConcurrentDictionary _voiceStates; + private readonly ConcurrentDictionary _emotes; + private readonly Dictionary> _boostSubscriptions; - private AudioClient _audioClient; + private AudioClient? _audioClient; private readonly SemaphoreSlim _audioLock; - private TaskCompletionSource _audioConnectPromise; + private TaskCompletionSource? _audioConnectPromise; /// public string Name { get; private set; } @@ -45,7 +45,7 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable public ulong OwnerId { get; private set; } /// Gets the user that owns this guild. - public SocketGuildUser Owner => GetUser(OwnerId); + public SocketGuildUser? Owner => GetUser(OwnerId); /// public string Icon { get; private set; } @@ -91,10 +91,10 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable /// /// TODO: To be documented. /// - public string AutoDeleteTime { get; private set; } + public string? AutoDeleteTime { get; private set; } /// - public RecommendInfo RecommendInfo { get; private set; } + public RecommendInfo? RecommendInfo { get; private set; } /// /// Gets the number of members. @@ -143,7 +143,7 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable /// /// Gets the associated with this guild. /// - public IAudioClient AudioClient => _audioClient; + public IAudioClient? AudioClient => _audioClient; /// public ulong MaxUploadLimit => GuildHelper.GetUploadLimit(this); @@ -151,7 +151,11 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable /// /// Gets the current logged-in user. /// - public SocketGuildUser CurrentUser => _members.TryGetValue(Kook.CurrentUser.Id, out SocketGuildUser member) ? member : null; + public SocketGuildUser? CurrentUser => + Kook.CurrentUser is not null + && _members.TryGetValue(Kook.CurrentUser.Id, out SocketGuildUser? member) + ? member + : null; /// /// Gets the built-in role containing all users in this guild. @@ -159,7 +163,7 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable /// /// A role object that represents an @everyone role in this guild. /// - public SocketRole EveryoneRole => GetRole(0); + public SocketRole EveryoneRole => GetRole(0) ?? new SocketRole(this, 0); /// /// Gets a collection of all text channels in this guild. @@ -167,8 +171,7 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable, IUpdateable /// /// A read-only collection of message channels found within this guild. /// - public IReadOnlyCollection TextChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection TextChannels => [..Channels.OfType()]; /// /// Gets a collection of all voice channels in this guild. @@ -176,8 +179,7 @@ public IReadOnlyCollection TextChannels /// /// A read-only collection of voice channels found within this guild. /// - public IReadOnlyCollection VoiceChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection VoiceChannels => [..Channels.OfType()]; /// /// Gets a collection of all stage channels in this guild. @@ -191,8 +193,7 @@ public IReadOnlyCollection VoiceChannels /// /// A read-only collection of category channels found within this guild. /// - public IReadOnlyCollection CategoryChannels - => Channels.OfType().ToImmutableArray(); + public IReadOnlyCollection CategoryChannels => [..Channels.OfType()]; /// /// Gets a collection of all channels in this guild. @@ -200,15 +201,7 @@ public IReadOnlyCollection CategoryChannels /// /// A read-only collection of generic channels found within this guild. /// - public IReadOnlyCollection Channels - { - get - { - ConcurrentDictionary channels = _channels; - ClientState state = Kook.State; - return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); - } - } + public IReadOnlyCollection Channels => [.._channels.Values]; /// /// Gets the default text channel for this guild. @@ -219,9 +212,9 @@ public IReadOnlyCollection Channels /// /// A representing the default text channel for this guild. /// - public SocketTextChannel DefaultChannel => TextChannels - .Where(c => CurrentUser?.GetPermissions(c).ViewChannel is true) - .SingleOrDefault(c => c.Id == DefaultChannelId); + public SocketTextChannel? DefaultChannel => TextChannels + .Where(x => CurrentUser?.GetPermissions(x).ViewChannel is true) + .FirstOrDefault(c => c.Id == DefaultChannelId); /// /// Gets the welcome text channel for this guild. @@ -232,13 +225,12 @@ public IReadOnlyCollection Channels /// /// A representing the default text channel for this guild. /// - public SocketTextChannel WelcomeChannel => TextChannels + public SocketTextChannel? WelcomeChannel => TextChannels .Where(c => CurrentUser?.GetPermissions(c).ViewChannel is true) - .SingleOrDefault(c => c.Id == WelcomeChannelId); + .FirstOrDefault(c => c.Id == WelcomeChannelId); /// - public IReadOnlyCollection Emotes => _emotes - .Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(_emotes); + public IReadOnlyCollection Emotes => [.._emotes.Values]; /// /// Gets a dictionary of all boost subscriptions for this guild. @@ -264,7 +256,7 @@ public IReadOnlyCollection Channels /// /// public ImmutableDictionary> BoostSubscriptions => - _boostSubscriptions?.ToImmutableDictionary(); + _boostSubscriptions.ToImmutableDictionary(); /// /// Gets a dictionary of all boost subscriptions which have not expired for this guild. @@ -290,11 +282,10 @@ public IReadOnlyCollection Channels /// /// public ImmutableDictionary> ValidBoostSubscriptions => - _boostSubscriptions?.Select(x => - new KeyValuePair>(x.Key, x.Value - .Where(y => y.IsValid) - .ToImmutableArray() as IReadOnlyCollection)) - .Where(x => x.Value.Any()) + _boostSubscriptions.Select(x => + new KeyValuePair>( + x.Key, [..x.Value.Where(y => y.IsValid)])) + .Where(x => x.Value.Count > 0) .ToImmutableDictionary(); /// @@ -340,7 +331,18 @@ public IReadOnlyCollection Channels internal SocketGuild(KookSocketClient kook, ulong id) : base(kook, id) { + _channels = []; + _members = []; + _roles = []; + _voiceStates = []; + _emotes = []; + _boostSubscriptions = []; _audioLock = new SemaphoreSlim(1, 1); + Name = string.Empty; + Topic = string.Empty; + Icon = string.Empty; + Banner = string.Empty; + Region = string.Empty; } internal static SocketGuild Create(KookSocketClient client, ClientState state, Model model) @@ -359,58 +361,54 @@ internal static SocketGuild Create(KookSocketClient client, ClientState state, R internal void Update(ClientState state, IReadOnlyCollection models) { - ConcurrentDictionary channels = new( - ConcurrentHashSet.DefaultConcurrencyLevel, - (int)(models.Count * 1.05)); foreach (ChannelModel model in models) { SocketGuildChannel channel = SocketGuildChannel.Create(this, state, model); state.AddChannel(channel); - channels.TryAdd(channel.Id, channel); + _channels.TryAdd(channel.Id, channel); } - - _channels = channels; } internal void Update(ClientState state, IReadOnlyCollection models) { - ConcurrentDictionary members = new(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Count * 1.05)); foreach (MemberModel model in models) { SocketGuildUser member = SocketGuildUser.Create(this, state, model); - if (members.TryAdd(member.Id, member)) member.GlobalUser.AddRef(); + if (_members.TryAdd(member.Id, member)) + member.GlobalUser.AddRef(); } - - DownloadedMemberCount = members.Count; - _members = members; - MemberCount = members.Count; + DownloadedMemberCount = _members.Count; + MemberCount = _members.Count; } - internal void Update(ClientState state, IReadOnlyCollection models) => - _boostSubscriptions = models.GroupBy(x => x.UserId) - .ToDictionary(x => RestUser.Create(Kook, x.First().User) as IUser, - x => x.GroupBy(y => (y.StartTime, y.EndTime)) - .Select(y => new BoostSubscriptionMetadata(y.Key.StartTime, y.Key.EndTime, y.Count())) - .ToImmutableArray() as IReadOnlyCollection); + internal void Update(ClientState state, IReadOnlyCollection models) + { + foreach (IGrouping group in models.GroupBy(x => x.UserId)) + { + SocketGlobalUser user = state.GetOrAddUser( + group.Key, _ => SocketGlobalUser.Create(Kook, state, group.First().User)); + IReadOnlyCollection subscriptions = + [ + ..group.GroupBy(x => (x.StartTime, x.EndTime)) + .Select(x => new BoostSubscriptionMetadata(x.Key.StartTime, x.Key.EndTime, x.Count())) + ]; + _boostSubscriptions[user] = subscriptions; + } + } internal void Update(ClientState state, RichModel model) { Update(state, model as ExtendedModel); - Banner = model.Banner; - if (model.Emojis != null) - { - _emotes.Clear(); - foreach (API.Emoji emoji in model.Emojis) _emotes.TryAdd(emoji.Id, emoji.ToEntity(model.Id)); - } - + _emotes.Clear(); + foreach (API.Emoji emoji in model.Emojis) + _emotes.TryAdd(emoji.Id, emoji.ToEntity(model.Id)); AddOrUpdateCurrentUser(model); } internal void Update(ClientState state, ExtendedModel model) { Update(state, model as Model); - Features = model.Features; BoostSubscriptionCount = model.BoostSubscriptionCount; BufferBoostSubscriptionCount = model.BufferBoostSubscriptionCount; @@ -440,37 +438,36 @@ internal void Update(ClientState state, Model model) IsAvailable = true; - ConcurrentDictionary roles = new(ConcurrentHashSet.DefaultConcurrencyLevel, - (int)((model.Roles?.Length ?? 0) * 1.05)); - for (int i = 0; i < (model.Roles?.Length ?? 0); i++) + if (model.Roles is { Length: > 0}) { - SocketRole role = SocketRole.Create(this, state, model.Roles![i]); - roles.TryAdd(role.Id, role); + foreach (RoleModel roleModel in model.Roles) + { + SocketRole role = SocketRole.Create(this, state, roleModel); + _roles.TryAdd(role.Id, role); + } } - _roles = roles; - - int channelCount = model.Channels?.Length ?? 0; - channelCount += (model.Channels ?? Array.Empty()).Sum(x => x.Channels?.Length ?? 0); - ConcurrentDictionary channels = new( - ConcurrentHashSet.DefaultConcurrencyLevel, - (int)(channelCount * 1.05)); - for (int i = 0; i < (model.Channels?.Length ?? 0); i++) + + if (model.Channels is not null) { - SocketGuildChannel channel = SocketGuildChannel.Create(this, state, model.Channels![i]); - state.AddChannel(channel); - channels.TryAdd(channel.Id, channel); - for (int j = 0; j < (model.Channels[i].Channels?.Length ?? 0); j++) + int channelCount = model.Channels.Length + + model.Channels.Sum(x => x.Channels?.Length ?? 0); + ConcurrentDictionary channels = new( + ConcurrentHashSet.DefaultConcurrencyLevel, + (int)(channelCount * 1.05)); + foreach (ChannelModel channelModel in model.Channels) { - SocketGuildChannel nestedChannel = SocketGuildChannel.Create(this, state, model.Channels![i].Channels![j]); - state.AddChannel(nestedChannel); - channels.TryAdd(nestedChannel.Id, nestedChannel); + SocketGuildChannel channel = SocketGuildChannel.Create(this, state, channelModel); + state.AddChannel(channel); + channels.TryAdd(channel.Id, channel); + if (channelModel.Channels is not { Length: > 0 }) continue; + foreach (ChannelModel nestedChannelModel in channelModel.Channels) + { + SocketGuildChannel nestedChannel = SocketGuildChannel.Create(this, state, nestedChannelModel); + state.AddChannel(nestedChannel); + _channels.TryAdd(nestedChannel.Id, nestedChannel); + } } } - _channels = channels; - - _members ??= new ConcurrentDictionary(); - _voiceStates ??= new ConcurrentDictionary(); - _emotes ??= new ConcurrentDictionary(); } internal void Update(ClientState state, GuildEvent model) @@ -492,8 +489,8 @@ internal void Update(ClientState state, GuildEvent model) } /// - public Task UpdateAsync(RequestOptions options = null) - => SocketGuildHelper.UpdateAsync(this, Kook, options); + public Task UpdateAsync(RequestOptions? options = null) => + SocketGuildHelper.UpdateAsync(this, Kook, options); #endregion @@ -506,54 +503,55 @@ public Task UpdateAsync(RequestOptions options = null) public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; - internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; + internal SocketGuild Clone() => (SocketGuild)MemberwiseClone(); #region General /// - public Task LeaveAsync(RequestOptions options = null) - => GuildHelper.LeaveAsync(this, Kook, options); + public Task LeaveAsync(RequestOptions? options = null) => + GuildHelper.LeaveAsync(this, Kook, options); /// - public Task>> GetBoostSubscriptionsAsync(RequestOptions options = null) - => SocketGuildHelper.GetBoostSubscriptionsAsync(this, Kook, options); + public Task>> GetBoostSubscriptionsAsync( + RequestOptions? options = null) => + SocketGuildHelper.GetBoostSubscriptionsAsync(this, Kook, options); /// - public Task>> GetActiveBoostSubscriptionsAsync( - RequestOptions options = null) - => SocketGuildHelper.GetActiveBoostSubscriptionsAsync(this, Kook, options); + public Task>> + GetActiveBoostSubscriptionsAsync(RequestOptions? options = null) => + SocketGuildHelper.GetActiveBoostSubscriptionsAsync(this, Kook, options); #endregion #region Bans /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Kook, options); + public Task> GetBansAsync(RequestOptions? options = null) => + GuildHelper.GetBansAsync(this, Kook, options); /// - public Task GetBanAsync(IUser user, RequestOptions options = null) - => GuildHelper.GetBanAsync(this, Kook, user.Id, options); + public Task GetBanAsync(IUser user, RequestOptions? options = null) => + GuildHelper.GetBanAsync(this, Kook, user.Id, options); /// - public Task GetBanAsync(ulong userId, RequestOptions options = null) - => GuildHelper.GetBanAsync(this, Kook, userId, options); + public Task GetBanAsync(ulong userId, RequestOptions? options = null) => + GuildHelper.GetBanAsync(this, Kook, userId, options); /// - public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Kook, user.Id, pruneDays, reason, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string? reason = null, RequestOptions? options = null) => + GuildHelper.AddBanAsync(this, Kook, user.Id, pruneDays, reason, options); /// - public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Kook, userId, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string? reason = null, RequestOptions? options = null) => + GuildHelper.AddBanAsync(this, Kook, userId, pruneDays, reason, options); /// - public Task RemoveBanAsync(IUser user, RequestOptions options = null) - => GuildHelper.RemoveBanAsync(this, Kook, user.Id, options); + public Task RemoveBanAsync(IUser user, RequestOptions? options = null) => + GuildHelper.RemoveBanAsync(this, Kook, user.Id, options); /// - public Task RemoveBanAsync(ulong userId, RequestOptions options = null) - => GuildHelper.RemoveBanAsync(this, Kook, userId, options); + public Task RemoveBanAsync(ulong userId, RequestOptions? options = null) => + GuildHelper.RemoveBanAsync(this, Kook, userId, options); #endregion @@ -566,13 +564,7 @@ public Task RemoveBanAsync(ulong userId, RequestOptions options = null) /// /// A generic channel associated with the specified ; null if none is found. /// - public SocketGuildChannel GetChannel(ulong id) - { - SocketGuildChannel channel = Kook.State.GetChannel(id) as SocketGuildChannel; - if (channel?.Guild.Id == Id) return channel; - - return null; - } + public SocketGuildChannel? GetChannel(ulong id) => Kook.State.GetChannel(id) as SocketGuildChannel; /// /// Gets a text channel in this guild. @@ -581,8 +573,7 @@ public SocketGuildChannel GetChannel(ulong id) /// /// A text channel associated with the specified ; null if none is found. /// - public SocketTextChannel GetTextChannel(ulong id) - => GetChannel(id) as SocketTextChannel; + public SocketTextChannel? GetTextChannel(ulong id) => GetChannel(id) as SocketTextChannel; /// /// Gets a voice channel in this guild. @@ -591,8 +582,7 @@ public SocketTextChannel GetTextChannel(ulong id) /// /// A voice channel associated with the specified ; null if none is found. /// - public SocketVoiceChannel GetVoiceChannel(ulong id) - => GetChannel(id) as SocketVoiceChannel; + public SocketVoiceChannel? GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; /// /// Gets a category channel in this guild. @@ -601,8 +591,7 @@ public SocketVoiceChannel GetVoiceChannel(ulong id) /// /// A category channel associated with the specified ; null if none is found. /// - public SocketCategoryChannel GetCategoryChannel(ulong id) - => GetChannel(id) as SocketCategoryChannel; + public SocketCategoryChannel? GetCategoryChannel(ulong id) => GetChannel(id) as SocketCategoryChannel; /// /// Creates a new text channel in this guild. @@ -614,8 +603,9 @@ public SocketCategoryChannel GetCategoryChannel(ulong id) /// A task that represents the asynchronous creation operation. The task result contains the newly created /// text channel. /// - public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) - => GuildHelper.CreateTextChannelAsync(this, Kook, name, options, func); + public Task CreateTextChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateTextChannelAsync(this, Kook, name, func, options); /// /// Creates a new voice channel in this guild. @@ -628,9 +618,9 @@ public Task CreateTextChannelAsync(string name, Action - public Task CreateVoiceChannelAsync(string name, Action func = null, - RequestOptions options = null) - => GuildHelper.CreateVoiceChannelAsync(this, Kook, name, options, func); + public Task CreateVoiceChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateVoiceChannelAsync(this, Kook, name, func, options); /// /// Creates a new channel category in this guild. @@ -643,9 +633,9 @@ public Task CreateVoiceChannelAsync(string name, Action - public Task CreateCategoryChannelAsync(string name, Action func = null, - RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Kook, name, options, func); + public Task CreateCategoryChannelAsync(string name, + Action? func = null, RequestOptions? options = null) => + GuildHelper.CreateCategoryChannelAsync(this, Kook, name, func, options); internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { @@ -657,29 +647,27 @@ internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) { - if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) - channel.Update(Kook.State, model); - else + if (_channels.TryGetValue(model.Id, out SocketGuildChannel? cachedChannel)) { - channel = SocketGuildChannel.Create(this, Kook.State, model); - _channels[channel.Id] = channel; - state.AddChannel(channel); + cachedChannel.Update(Kook.State, model); + return cachedChannel; } + SocketGuildChannel channel = SocketGuildChannel.Create(this, Kook.State, model); + _channels[channel.Id] = channel; + state.AddChannel(channel); return channel; } - internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) - { - if (_channels.TryRemove(id, out SocketGuildChannel _)) return state.RemoveChannel(id) as SocketGuildChannel; - - return null; - } + internal SocketGuildChannel? RemoveChannel(ClientState state, ulong id) => + _channels.TryRemove(id, out SocketGuildChannel? _) + ? state.RemoveChannel(id) as SocketGuildChannel + : null; internal void PurgeChannelCache(ClientState state) { - foreach (KeyValuePair channelId in _channels) state.RemoveChannel(channelId.Key); - + foreach (KeyValuePair channelId in _channels) + state.RemoveChannel(channelId.Key); _channels.Clear(); } @@ -695,7 +683,7 @@ internal void PurgeChannelCache(ClientState state) // /// A task that represents the asynchronous get operation. The task result contains a read-only collection of // /// invite metadata, each representing information for an invite found within this guild. // /// - // public Task> GetInvitesAsync(RequestOptions options = null) + // public Task> GetInvitesAsync(RequestOptions? options = null) // => GuildHelper.GetInvitesAsync(this, Kook, options); // // #endregion @@ -709,12 +697,8 @@ internal void PurgeChannelCache(ClientState state) /// /// A role that is associated with the specified ; null if none is found. /// - public SocketRole GetRole(uint id) - { - if (_roles.TryGetValue(id, out SocketRole value)) return value; - - return null; - } + public SocketRole? GetRole(uint id) => + _roles.TryGetValue(id, out SocketRole? value) ? value : null; /// /// Creates a new role with the provided name. @@ -726,8 +710,8 @@ public SocketRole GetRole(uint id) /// A task that represents the asynchronous creation operation. The task result contains the newly created /// role. /// - public Task CreateRoleAsync(string name, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Kook, name, options); + public Task CreateRoleAsync(string name, RequestOptions? options = null) => + GuildHelper.CreateRoleAsync(this, Kook, name, options); internal SocketRole AddRole(RoleModel model) { @@ -736,20 +720,14 @@ internal SocketRole AddRole(RoleModel model) return role; } - internal SocketRole RemoveRole(uint id) - { - if (_roles.TryRemove(id, out SocketRole role)) return role; - - return null; - } + internal SocketRole? RemoveRole(uint id) => + _roles.TryRemove(id, out SocketRole? role) ? role : null; internal SocketRole AddOrUpdateRole(RoleModel model) { - if (_roles.TryGetValue(model.Id, out SocketRole role)) - _roles[model.Id].Update(Kook.State, model); - else - role = AddRole(model); - + if (!_roles.TryGetValue(model.Id, out SocketRole? role)) + return AddRole(model); + _roles[model.Id].Update(Kook.State, model); return role; } @@ -771,52 +749,44 @@ internal SocketRole AddOrUpdateRole(RoleModel model) /// /// A guild user associated with the specified ; null if none is found. /// - public SocketGuildUser GetUser(ulong id) - { - if (_members.TryGetValue(id, out SocketGuildUser member)) return member; - - return null; - } + public SocketGuildUser? GetUser(ulong id) => + _members.TryGetValue(id, out SocketGuildUser? member) ? member : null; internal SocketGuildUser AddOrUpdateUser(UserModel model) { - if (_members.TryGetValue(model.Id, out SocketGuildUser member)) + if (_members.TryGetValue(model.Id, out SocketGuildUser? cachedMember)) { - member.GlobalUser?.Update(Kook.State, model); - member.UpdatePresence(model.Online, model.OperatingSystem); - } - else - { - member = SocketGuildUser.Create(this, Kook.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; - DownloadedMemberCount++; + cachedMember.GlobalUser?.Update(Kook.State, model); + cachedMember.UpdatePresence(model.Online, model.OperatingSystem); + return cachedMember; } + SocketGuildUser member = SocketGuildUser.Create(this, Kook.State, model); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; return member; } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { - if (_members.TryGetValue(model.Id, out SocketGuildUser member)) - { - member.Update(Kook.State, model); - member.UpdatePresence(model.Online, model.OperatingSystem); - } - else + if (_members.TryGetValue(model.Id, out SocketGuildUser? cachedMember)) { - member = SocketGuildUser.Create(this, Kook.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; - DownloadedMemberCount++; + cachedMember.Update(Kook.State, model); + cachedMember.UpdatePresence(model.Online, model.OperatingSystem); + return cachedMember; } + SocketGuildUser member = SocketGuildUser.Create(this, Kook.State, model); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; return member; } internal SocketGuildUser AddOrUpdateCurrentUser(RichModel model) { - if (_members.TryGetValue(Kook.CurrentUser.Id, out SocketGuildUser member)) + if (Kook.CurrentUser is not null && _members.TryGetValue(Kook.CurrentUser.Id, out SocketGuildUser? member)) { member.Update(Kook.State, model); } @@ -831,16 +801,13 @@ internal SocketGuildUser AddOrUpdateCurrentUser(RichModel model) return member; } - internal SocketGuildUser RemoveUser(ulong id) + internal SocketGuildUser? RemoveUser(ulong id) { - if (_members.TryRemove(id, out SocketGuildUser member)) - { - DownloadedMemberCount--; - member.GlobalUser.RemoveRef(Kook); - return member; - } - - return null; + if (!_members.TryRemove(id, out SocketGuildUser? member)) + return null; + DownloadedMemberCount--; + member.GlobalUser.RemoveRef(Kook); + return member; } /// @@ -854,15 +821,17 @@ internal SocketGuildUser RemoveUser(ulong id) /// The predicate used to select which users to clear. public void PurgeUserCache(Func predicate) { - IEnumerable membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Kook.CurrentUser.Id); - IEnumerable membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Kook.CurrentUser.Id); - + IEnumerable membersToPurge = Users + .Where(x => predicate.Invoke(x) && x.Id != Kook.CurrentUser?.Id); + IEnumerable membersToKeep = Users + .Where(x => !predicate.Invoke(x) || x.Id == Kook.CurrentUser?.Id); foreach (SocketGuildUser member in membersToPurge) + { if (_members.TryRemove(member.Id, out _)) member.GlobalUser.RemoveRef(Kook); - - foreach (SocketGuildUser member in membersToKeep) _members.TryAdd(member.Id, member); - + } + foreach (SocketGuildUser member in membersToKeep) + _members.TryAdd(member.Id, member); DownloadedMemberCount = _members.Count; } @@ -878,23 +847,23 @@ public void PurgeUserCache(Func predicate) /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users found within this guild. /// - public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + public IAsyncEnumerable> GetUsersAsync(RequestOptions? options = null) { - if (HasAllMembers is true) return ImmutableArray.Create(Users).ToAsyncEnumerable>(); - + if (HasAllMembers is true) + return ImmutableArray.Create(Users).ToAsyncEnumerable>(); return GuildHelper.GetUsersAsync(this, Kook, KookConfig.MaxUsersPerBatch, 1, options); } /// - public async Task DownloadUsersAsync(RequestOptions options = null) => + public async Task DownloadUsersAsync(RequestOptions? options = null) => await Kook.DownloadUsersAsync(new[] { this }, options).ConfigureAwait(false); /// - public async Task DownloadVoiceStatesAsync(RequestOptions options = null) => + public async Task DownloadVoiceStatesAsync(RequestOptions? options = null) => await Kook.DownloadVoiceStatesAsync(new[] { this }, options).ConfigureAwait(false); /// - public async Task DownloadBoostSubscriptionsAsync(RequestOptions options = null) => + public async Task DownloadBoostSubscriptionsAsync(RequestOptions? options = null) => await Kook.DownloadBoostSubscriptionsAsync(new[] { this }, options).ConfigureAwait(false); /// @@ -912,17 +881,19 @@ public async Task DownloadBoostSubscriptionsAsync(RequestOptions options = null) /// users that matches the properties with the provided /// at . /// - public IAsyncEnumerable> SearchUsersAsync(Action func, - int limit = KookConfig.MaxUsersPerBatch, RequestOptions options = null) - => GuildHelper.SearchUsersAsync(this, Kook, func, limit, options); + public IAsyncEnumerable> SearchUsersAsync( + Action func, int limit = KookConfig.MaxUsersPerBatch, + RequestOptions? options = null) => + GuildHelper.SearchUsersAsync(this, Kook, func, limit, options); #endregion #region Voices /// - public async Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, RequestOptions options = null) - => await ClientHelper.MoveUsersAsync(Kook, users, targetChannel, options).ConfigureAwait(false); + public Task MoveUsersAsync(IEnumerable users, IVoiceChannel targetChannel, + RequestOptions? options = null) => + ClientHelper.MoveUsersAsync(Kook, users, targetChannel, options); #endregion @@ -935,19 +906,8 @@ public async Task MoveUsersAsync(IEnumerable users, IVoiceChannel ta /// /// A guild emoji associated with the specified ; null if none is found. /// - public GuildEmote GetEmote(string id) - { - if (_emotes.TryGetValue(id, out GuildEmote emote)) return emote; - - return null; - } - - internal GuildEmote AddEmote(GuildEmojiEvent model) - { - GuildEmote emote = model.ToEntity(Id); - _emotes.TryAdd(model.Id, emote); - return emote; - } + public GuildEmote? GetEmote(string id) => + _emotes.TryGetValue(id, out GuildEmote? emote) ? emote : null; internal GuildEmote AddOrUpdateEmote(GuildEmojiEvent model) { @@ -956,50 +916,47 @@ internal GuildEmote AddOrUpdateEmote(GuildEmojiEvent model) return emote; } - internal GuildEmote RemoveEmote(string id) - { - if (_emotes.TryRemove(id, out GuildEmote emote)) return emote; - - return null; - } + internal GuildEmote? RemoveEmote(string id) => + _emotes.TryRemove(id, out GuildEmote? emote) ? emote : null; /// - public Task> GetEmotesAsync(RequestOptions options = null) - => GuildHelper.GetEmotesAsync(this, Kook, options); + public Task> GetEmotesAsync(RequestOptions? options = null) => + GuildHelper.GetEmotesAsync(this, Kook, options); /// - public Task GetEmoteAsync(string id, RequestOptions options = null) - => GuildHelper.GetEmoteAsync(this, Kook, id, options); + public Task GetEmoteAsync(string id, RequestOptions? options = null) => + GuildHelper.GetEmoteAsync(this, Kook, id, options); /// - public Task CreateEmoteAsync(string name, Image image, RequestOptions options = null) - => GuildHelper.CreateEmoteAsync(this, Kook, name, image, options); + public Task CreateEmoteAsync(string name, Image image, RequestOptions? options = null) => + GuildHelper.CreateEmoteAsync(this, Kook, name, image, options); /// /// is null. - public Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions options = null) - => GuildHelper.ModifyEmoteNameAsync(this, Kook, emote, name, options); + public Task ModifyEmoteNameAsync(GuildEmote emote, string name, RequestOptions? options = null) => + GuildHelper.ModifyEmoteNameAsync(this, Kook, emote, name, options); /// - public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) - => GuildHelper.DeleteEmoteAsync(this, Kook, emote.Id, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions? options = null) => + GuildHelper.DeleteEmoteAsync(this, Kook, emote.Id, options); #endregion #region Invites /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await GuildHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); + public async Task> GetInvitesAsync(RequestOptions? options = null) => + await GuildHelper.GetInvitesAsync(this, Kook, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, RequestOptions options = null) - => await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 604800, + int? maxUses = null, RequestOptions? options = null) => + await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); /// - public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) - => await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); + public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => + await GuildHelper.CreateInviteAsync(this, Kook, maxAge, maxUses, options).ConfigureAwait(false); #endregion @@ -1007,7 +964,7 @@ public async Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge. internal SocketVoiceState AddOrUpdateVoiceState(ulong userId, ulong? voiceChannelId) { - SocketVoiceChannel voiceChannel = voiceChannelId.HasValue + SocketVoiceChannel? voiceChannel = voiceChannelId.HasValue ? GetChannel(voiceChannelId.Value) as SocketVoiceChannel : null; SocketVoiceState socketState = GetVoiceState(userId) ?? SocketVoiceState.Default; @@ -1024,34 +981,26 @@ internal SocketVoiceState AddOrUpdateVoiceState(ulong userId, bool? isMuted = nu return socketState; } - internal SocketVoiceState? GetVoiceState(ulong id) - { - if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; + internal SocketVoiceState? GetVoiceState(ulong id) => + _voiceStates.TryGetValue(id, out SocketVoiceState voiceState) ? voiceState : null; - return null; - } - - internal SocketVoiceState? RemoveVoiceState(ulong id) - { - if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) return voiceState; - - return null; - } + internal SocketVoiceState? RemoveVoiceState(ulong id) => + _voiceStates.TryRemove(id, out SocketVoiceState voiceState) ? voiceState : null; #endregion #region Audio - internal async Task ConnectAudioAsync(ulong channelId, /*bool selfDeaf, bool selfMute, */bool external, bool disconnect) + internal async Task ConnectAudioAsync(ulong channelId, + /*bool selfDeaf, bool selfMute, */bool external, bool disconnect) { - TaskCompletionSource promise; - + TaskCompletionSource promise; await _audioLock.WaitAsync().ConfigureAwait(false); try { if (disconnect || !external) await DisconnectAudioInternalAsync().ConfigureAwait(false); - promise = new TaskCompletionSource(); + promise = new TaskCompletionSource(); _audioConnectPromise = promise; if (external) @@ -1061,9 +1010,9 @@ internal async Task ConnectAudioAsync(ulong channelId, /*bool self return null; } - if (_audioClient == null) + if (_audioClient is null) { - var audioClient = new AudioClient(this, Kook.GetAudioId(), channelId); + AudioClient audioClient = new(this, Kook.GetAudioId(), channelId); audioClient.Disconnected += async ex => { if (!promise.Task.IsCompleted) @@ -1077,7 +1026,7 @@ internal async Task ConnectAudioAsync(ulong channelId, /*bool self // ignored } _audioClient = null; - if (ex != null) + if (ex is not null) await promise.TrySetExceptionAsync(ex); else await promise.TrySetCanceledAsync(); @@ -1106,9 +1055,10 @@ internal async Task ConnectAudioAsync(ulong channelId, /*bool self try { - var timeoutTask = Task.Delay(15000); - if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask) - throw new TimeoutException(); + Task timeoutTask = Task.Delay(15000); + Task completedTask = await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false); + if (completedTask == timeoutTask) + throw new TimeoutException("The audio client failed to connect within 15 seconds."); return await promise.Task.ConfigureAwait(false); } catch @@ -1120,13 +1070,19 @@ internal async Task ConnectAudioAsync(ulong channelId, /*bool self private async Task UpdateSelfVoiceStateAsync(ulong channelId, bool selfDeaf, bool selfMute) { - SocketGuildUser selfUser = GetVoiceChannel(channelId).Users - .SingleOrDefault(x => x.Id == Kook.CurrentUser.Id); - if (selfUser is null) return; - if (selfDeaf) await selfUser.DeafenAsync(); - else await selfUser.UndeafenAsync(); - if (selfMute) await selfUser.MuteAsync(); - else await selfUser.UnmuteAsync(); + SocketGuildUser? selfUser = GetVoiceChannel(channelId)? + .Users + .SingleOrDefault(x => x.Id == Kook.CurrentUser?.Id); + if (selfUser is null) + return; + if (selfDeaf) + await selfUser.DeafenAsync(); + else + await selfUser.UndeafenAsync(); + if (selfMute) + await selfUser.MuteAsync(); + else + await selfUser.UnmuteAsync(); } internal async Task DisconnectAudioAsync() @@ -1146,7 +1102,7 @@ private async Task DisconnectAudioInternalAsync() { _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection _audioConnectPromise = null; - if (_audioClient != null) + if (_audioClient is not null) await _audioClient.StopAsync().ConfigureAwait(false); _audioClient?.Dispose(); _audioClient = null; @@ -1157,18 +1113,21 @@ private async Task DisconnectAudioInternalAsync() #region IGuild /// - IAudioClient IGuild.AudioClient => AudioClient; + IAudioClient? IGuild.AudioClient => AudioClient; /// bool IGuild.Available => true; /// - public void Dispose() + void IDisposable.Dispose() { + DisconnectAudioAsync().GetAwaiter().GetResult(); + _audioLock?.Dispose(); + _audioClient?.Dispose(); } /// - async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions? options) { if (mode == CacheMode.AllowDownload && HasAllMembers is not true) return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); @@ -1177,26 +1136,23 @@ async Task> IGuild.GetUsersAsync(CacheMode mode, } /// - Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetUser(id)); /// - Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(CurrentUser); + Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(CurrentUser); /// - Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(Owner); + Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(Owner); /// - IAsyncEnumerable> IGuild.SearchUsersAsync(Action func, int limit, CacheMode mode, - RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return SearchUsersAsync(func, limit, options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IGuild.SearchUsersAsync( + Action func, int limit, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? SearchUsersAsync(func, limit, options) + : AsyncEnumerable.Empty>(); /// IRole IGuild.EveryoneRole => EveryoneRole; @@ -1208,78 +1164,81 @@ IAsyncEnumerable> IGuild.SearchUsersAsync(Action IReadOnlyCollection IGuild.Emotes => Emotes; /// - IRole IGuild.GetRole(uint id) => GetRole(id); + IRole? IGuild.GetRole(uint id) => GetRole(id); /// - IRecommendInfo IGuild.RecommendInfo => RecommendInfo; + IRecommendInfo? IGuild.RecommendInfo => RecommendInfo; /// - async Task IGuild.CreateRoleAsync(string name, RequestOptions options) - => await CreateRoleAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateRoleAsync(string name, RequestOptions? options) => + await CreateRoleAsync(name, options).ConfigureAwait(false); /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + async Task> IGuild.GetBansAsync(RequestOptions? options) => + await GetBansAsync(options).ConfigureAwait(false); /// - async Task IGuild.GetBanAsync(IUser user, RequestOptions options) - => await GetBanAsync(user, options).ConfigureAwait(false); + async Task IGuild.GetBanAsync(IUser user, RequestOptions? options) => + await GetBanAsync(user, options).ConfigureAwait(false); /// - async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) - => await GetBanAsync(userId, options).ConfigureAwait(false); + async Task IGuild.GetBanAsync(ulong userId, RequestOptions? options) => + await GetBanAsync(userId, options).ConfigureAwait(false); /// - Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(Channels); + Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(Channels); /// - Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetChannel(id)); + Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetChannel(id)); /// - Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(DefaultChannel); + Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(DefaultChannel); /// - Task IGuild.GetWelcomeChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(WelcomeChannel); + Task IGuild.GetWelcomeChannelAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult(WelcomeChannel); /// - Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(TextChannels); + Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(TextChannels); /// - Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetTextChannel(id)); + Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetTextChannel(id)); /// - Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(VoiceChannels); + Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(VoiceChannels); /// - Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetVoiceChannel(id)); + Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetVoiceChannel(id)); /// - Task> IGuild.GetCategoryChannelsAsync(CacheMode mode, - RequestOptions options) - => Task.FromResult>(CategoryChannels); + Task> IGuild.GetCategoryChannelsAsync( + CacheMode mode, RequestOptions? options) => + Task.FromResult>(CategoryChannels); /// - async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) - => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateTextChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) - => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateCategoryChannelAsync(string name, Action func, RequestOptions options) - => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryChannelAsync(string name, + Action? func, RequestOptions? options) => + await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); /// - public async Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions options = null) => + public async Task GetBadgeAsync(BadgeStyle style = BadgeStyle.GuildName, RequestOptions? options = null) => await GuildHelper.GetBadgeAsync(this, Kook, style, options).ConfigureAwait(false); #endregion diff --git a/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuildHelper.cs b/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuildHelper.cs index 79e32fff..27449e32 100644 --- a/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuildHelper.cs +++ b/src/Kook.Net.WebSocket/Entities/Guilds/SocketGuildHelper.cs @@ -6,23 +6,22 @@ namespace Kook.WebSocket; internal static class SocketGuildHelper { - public static async Task UpdateAsync(SocketGuild guild, KookSocketClient client, - RequestOptions options) + public static async Task UpdateAsync(SocketGuild guild, KookSocketClient client, RequestOptions? options) { ExtendedGuild extendedGuild = await client.ApiClient.GetGuildAsync(guild.Id, options).ConfigureAwait(false); if (client.AlwaysDownloadBoostSubscriptions && (guild.BoostSubscriptionCount != extendedGuild.BoostSubscriptionCount || guild.BufferBoostSubscriptionCount != extendedGuild.BufferBoostSubscriptionCount)) await guild.DownloadBoostSubscriptionsAsync(); - guild.Update(client.State, extendedGuild); } public static async Task>> GetBoostSubscriptionsAsync( - SocketGuild guild, BaseSocketClient client, RequestOptions options) + SocketGuild guild, BaseSocketClient client, RequestOptions? options) { IEnumerable subscriptions = await client.ApiClient - .GetGuildBoostSubscriptionsAsync(guild.Id, options: options).FlattenAsync(); + .GetGuildBoostSubscriptionsAsync(guild.Id, options: options) + .FlattenAsync(); return subscriptions.GroupBy(x => x.UserId) .ToImmutableDictionary(x => guild.GetUser(x.Key) ?? client.GetUser(x.Key) ?? RestUser.Create(client, x.First().User) as IUser, x => x.GroupBy(y => (y.StartTime, y.EndTime)) @@ -31,7 +30,7 @@ public static async Task>> GetActiveBoostSubscriptionsAsync( - SocketGuild guild, BaseSocketClient client, RequestOptions options) + SocketGuild guild, BaseSocketClient client, RequestOptions? options) { IEnumerable subscriptions = await client.ApiClient .GetGuildBoostSubscriptionsAsync(guild.Id, DateTimeOffset.Now.Add(-KookConfig.BoostPackDuration), options: options).FlattenAsync(); diff --git a/src/Kook.Net.WebSocket/Entities/Invites/SocketInvite.cs b/src/Kook.Net.WebSocket/Entities/Invites/SocketInvite.cs index af04d654..2dd3cbf2 100644 --- a/src/Kook.Net.WebSocket/Entities/Invites/SocketInvite.cs +++ b/src/Kook.Net.WebSocket/Entities/Invites/SocketInvite.cs @@ -7,7 +7,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based invite to a guild. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketInvite : SocketEntity, IInvite { /// @@ -26,31 +26,27 @@ public class SocketInvite : SocketEntity, IInvite /// public SocketGuild Guild { get; } - /// - /// Gets the time at which this invite will expire. - /// + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// public DateTimeOffset? ExpiresAt { get; private set; } - /// - /// Gets the time span until the invite expires. - /// + /// public TimeSpan? MaxAge { get; private set; } - /// - /// Gets the max number of uses this invite may have. - /// + /// public int? MaxUses { get; private set; } - /// - /// Gets the number of times this invite has been used. - /// + /// public int? Uses { get; private set; } - /// - /// Gets the number of times this invite still remains. - /// + /// public int? RemainingUses { get; private set; } + /// + public int InvitedUsersCount { get; private set; } + /// /// Gets the user that created this invite if available. /// @@ -62,15 +58,19 @@ public class SocketInvite : SocketEntity, IInvite /// public string Url { get; private set; } - internal SocketInvite(KookSocketClient kook, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, uint id) + internal SocketInvite(KookSocketClient kook, SocketGuild guild, + SocketGuildChannel channel, SocketGuildUser inviter, uint id) : base(kook, id) { Guild = guild; Channel = channel; Inviter = inviter; + Code = string.Empty; + Url = string.Empty; } - internal static SocketInvite Create(KookSocketClient kook, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, Model model) + internal static SocketInvite Create(KookSocketClient kook, SocketGuild guild, + SocketGuildChannel channel, SocketGuildUser inviter, Model model) { SocketInvite entity = new(kook, guild, channel, inviter, model.Id); entity.Update(model); @@ -82,17 +82,19 @@ internal void Update(Model model) Code = model.UrlCode; Url = model.Url; GuildId = model.GuildId; - ChannelId = model.ChannelId; + ChannelId = model.ChannelId != 0 ? model.ChannelId : null; + CreatedAt = model.CreatedAt; ExpiresAt = model.ExpiresAt; MaxAge = model.Duration; MaxUses = model.UsingTimes == -1 ? null : model.UsingTimes; RemainingUses = model.RemainingTimes == -1 ? null : model.RemainingTimes; Uses = MaxUses - RemainingUses; + InvitedUsersCount = model.InviteesCount; } /// - public Task DeleteAsync(RequestOptions options = null) - => InviteHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + InviteHelper.DeleteAsync(this, Kook, options); /// /// Gets the URL of the invite. @@ -114,14 +116,13 @@ public Task DeleteAsync(RequestOptions options = null) IUser IInvite.Inviter => Inviter; /// - ChannelType IInvite.ChannelType => - Channel switch - { - IVoiceChannel voiceChannel => ChannelType.Voice, - ICategoryChannel categoryChannel => ChannelType.Category, - ITextChannel textChannel => ChannelType.Text, - _ => throw new InvalidOperationException("Invalid channel type.") - }; + ChannelType IInvite.ChannelType => Channel switch + { + IVoiceChannel => ChannelType.Voice, + ICategoryChannel => ChannelType.Category, + ITextChannel => ChannelType.Text, + _ => throw new InvalidOperationException("Invalid channel type.") + }; /// string IInvite.ChannelName => Channel.Name; diff --git a/src/Kook.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Kook.Net.WebSocket/Entities/Messages/MessageCache.cs index f8641ea8..e8668516 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -20,68 +20,57 @@ public MessageCache(KookSocketClient kook) public void Add(SocketMessage message) { - if (_messages.TryAdd(message.Id, message)) - { - _orderedMessages.Enqueue((message.Id, message.Timestamp)); - - while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out (Guid MsgId, DateTimeOffset Timestamp) msg)) - _messages.TryRemove(msg.MsgId, out _); - } + if (!_messages.TryAdd(message.Id, message)) + return; + _orderedMessages.Enqueue((message.Id, message.Timestamp)); + while (_orderedMessages.Count > _size + && _orderedMessages.TryDequeue(out (Guid MsgId, DateTimeOffset Timestamp) msg)) + _messages.TryRemove(msg.MsgId, out _); } - public SocketMessage Remove(Guid id) - { - _messages.TryRemove(id, out SocketMessage msg); - return msg; - } - - public SocketMessage Get(Guid id) - { - if (_messages.TryGetValue(id, out SocketMessage result)) return result; + public SocketMessage? Remove(Guid id) => + _messages.TryRemove(id, out SocketMessage? msg) ? msg : null; - return null; - } + public SocketMessage? Get(Guid id) => + _messages.TryGetValue(id, out SocketMessage? result) ? result : null; /// is less than 0. - public IReadOnlyCollection GetMany(Guid? referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch) + public IReadOnlyCollection GetMany(Guid? referenceMessageId, + Direction dir, int limit = KookConfig.MaxMessagesPerBatch) { - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - - if (limit == 0) return ImmutableArray.Empty; + if (limit < 0) + throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) + return []; SocketMessage referenceMessage = _messages.SingleOrDefault(x => x.Key == referenceMessageId).Value; - IEnumerable cachedMessageIds; if (referenceMessageId == null || dir == Direction.Unspecified) cachedMessageIds = _orderedMessages.Select(x => x.MsgId); else if (dir == Direction.Before) cachedMessageIds = _orderedMessages.Where(x => x.Timestamp < referenceMessage.Timestamp).Select(x => x.MsgId); else if (dir == Direction.After) - cachedMessageIds = _orderedMessages.Where(x => x.Timestamp < referenceMessage.Timestamp).Select(x => x.MsgId); + cachedMessageIds = _orderedMessages.Where(x => x.Timestamp > referenceMessage.Timestamp).Select(x => x.MsgId); else //Direction.Around { - if (!_messages.TryGetValue(referenceMessageId.Value, out SocketMessage msg)) return ImmutableArray.Empty; - + if (!_messages.TryGetValue(referenceMessageId.Value, out SocketMessage? msg)) + return []; int around = limit / 2; IReadOnlyCollection before = GetMany(referenceMessageId, Direction.Before, around); IEnumerable after = GetMany(referenceMessageId, Direction.After, around).Reverse(); - - return after.Concat(new[] { msg }).Concat(before).ToImmutableArray(); + return [..after, msg, ..before]; } - if (dir == Direction.Before) cachedMessageIds = cachedMessageIds.Reverse(); + if (dir == Direction.Before) + cachedMessageIds = cachedMessageIds.Reverse(); - if (dir == Direction.Around) //Only happens if referenceMessageId is null, should only get "around" and itself (+1) + //Only happens if referenceMessageId is null, should only get "around" and itself (+1) + if (dir == Direction.Around) limit = limit / 2 + 1; return cachedMessageIds - .Select(x => - { - if (_messages.TryGetValue(x, out SocketMessage msg)) return msg; - - return null; - }) - .Where(x => x != null) + .Select(x => _messages.TryGetValue(x, out SocketMessage? msg) ? msg : null) + .OfType() .Take(limit) .ToImmutableArray(); } diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketMessage.cs index 2d5c4673..9d6f957d 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -11,8 +11,8 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable { #region SocketMessage - private readonly List _reactions = new(); - private ImmutableArray _userMentions = ImmutableArray.Create(); + private readonly List _reactions = []; + private ImmutableArray _userMentions = []; /// /// Gets the author of this message. @@ -53,14 +53,14 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable /// public DateTimeOffset? EditedTimestamp { get; private set; } - /// - public virtual bool? IsPinned => null; + /// + public virtual bool IsPinned { get; protected internal set; } /// - public virtual bool? MentionedEveryone => false; + public virtual bool MentionedEveryone => false; /// - public virtual bool? MentionedHere => false; + public virtual bool MentionedHere => false; /// public MessageType Type { get; private set; } @@ -76,7 +76,7 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable /// /// Collection of card objects. /// - public virtual IReadOnlyCollection Cards => ImmutableArray.Create(); + public virtual IReadOnlyCollection Cards => []; /// /// Returns all embeds included in this message. @@ -84,7 +84,7 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable /// /// Collection of embed objects. /// - public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => []; /// /// Gets a collection of the 's on the message. @@ -92,7 +92,7 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable /// /// Collection of poke action objects. /// - public virtual IReadOnlyCollection Pokes => ImmutableArray.Create(); + public virtual IReadOnlyCollection Pokes => []; /// /// Returns the roles mentioned in this message. @@ -100,7 +100,7 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable /// /// Collection of WebSocket-based roles. /// - public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoles => []; /// /// Returns the users mentioned in this message. @@ -111,90 +111,86 @@ public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable public IReadOnlyCollection MentionedUsers => _userMentions; /// - public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + public virtual IReadOnlyCollection Tags => []; /// - public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, - x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Kook.CurrentUser.Id) }); + public IReadOnlyDictionary Reactions => _reactions + .GroupBy(r => r.Emote) + .ToDictionary( + x => x.Key, + x => new ReactionMetadata + { + ReactionCount = x.Count(), + IsMe = x.Any(y => y.UserId == Kook.CurrentUser?.Id) + }); - internal SocketMessage(KookSocketClient kook, Guid id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + internal SocketMessage(KookSocketClient kook, Guid id, ISocketMessageChannel channel, + SocketUser author, MessageSource source) : base(kook, id) { Channel = channel; Author = author; Source = source; + Content = string.Empty; + RawContent = string.Empty; + Attachments = []; } - internal static SocketMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) - { - if (model.Author.IsSystemUser ?? model.Author.Id == KookConfig.SystemMessageAuthorID) - return SocketSystemMessage.Create(kook, state, author, channel, model, gatewayEvent); - else - return SocketUserMessage.Create(kook, state, author, channel, model, gatewayEvent); - } - - internal virtual void Update(ClientState state, GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) + internal static SocketMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, + GatewayEvent gatewayEvent) => + MessageHelper.GetSource(author) is MessageSource.System + ? SocketSystemMessage.Create(kook, state, author, channel, gatewayEvent) + : SocketUserMessage.Create(kook, state, author, channel, gatewayEvent); + + internal static SocketMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, + GatewayEvent gatewayEvent) => + MessageHelper.GetSource(author) is MessageSource.System + ? SocketSystemMessage.Create(kook, state, author, channel, gatewayEvent) + : SocketUserMessage.Create(kook, state, author, channel, gatewayEvent); + + internal virtual void Update(ClientState state, GatewayEvent gatewayEvent) { Type = gatewayEvent.Type; Timestamp = gatewayEvent.MessageTimestamp; Content = gatewayEvent.Content; - if (model.MentionedUsers is not null) + if (gatewayEvent.ExtraData.MentionedUsers is { } users) { - ulong[] ids = model.MentionedUsers; - if (ids.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(ids.Length); - for (int i = 0; i < ids.Length; i++) - { - ulong id = ids[i]; - SocketUser user = Channel.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; - newMentions.Add(user ?? SocketUnknownUser.Create(Kook, state, id)); - } - - _userMentions = newMentions.ToImmutable(); - } + _userMentions = users.Select(x => + Channel.GetUserAsync(x, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser + ?? SocketUnknownUser.Create(Kook, state, x)) + .ToImmutableArray(); } } - internal static SocketMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) - { - if (model.Author.IsSystemUser ?? model.Author.Id == KookConfig.SystemMessageAuthorID) - return SocketSystemMessage.Create(kook, state, author, channel, model, gatewayEvent); - else - return SocketUserMessage.Create(kook, state, author, channel, model, gatewayEvent); - } - - internal virtual void Update(ClientState state, GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) + internal virtual void Update(ClientState state, GatewayEvent gatewayEvent) { Type = gatewayEvent.Type; Timestamp = gatewayEvent.MessageTimestamp; Content = gatewayEvent.Content; - } - internal static SocketMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.Message model) - { - if (model is null) return null; - - if (model.Author.IsSystemUser ?? model.Author.Id == KookConfig.SystemMessageAuthorID) - return SocketSystemMessage.Create(kook, state, author, channel, model); - else - return SocketUserMessage.Create(kook, state, author, channel, model); + if (gatewayEvent.ExtraData.MentionedUsers is { } users) + { + _userMentions = users.Select(x => + Channel.GetUserAsync(x, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser + ?? SocketUnknownUser.Create(Kook, state, x)) + .ToImmutableArray(); + } } - internal static SocketMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.DirectMessage model) - { - if (model is null) return null; + internal static SocketMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, API.Message model) => + MessageHelper.GetSource(author) is MessageSource.System + ? SocketSystemMessage.Create(kook, state, author, channel, model) + : SocketUserMessage.Create(kook, state, author, channel, model); - if (author.IsSystemUser ?? model.AuthorId == KookConfig.SystemMessageAuthorID) - return SocketSystemMessage.Create(kook, state, author, channel, model); - else - return SocketUserMessage.Create(kook, state, author, channel, model); - } + internal static SocketMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, API.DirectMessage model) => + MessageHelper.GetSource(author) is MessageSource.System + ? SocketSystemMessage.Create(kook, state, author, channel, model) + : SocketUserMessage.Create(kook, state, author, channel, model); internal virtual void Update(ClientState state, API.Message model) { @@ -203,21 +199,12 @@ internal virtual void Update(ClientState state, API.Message model) EditedTimestamp = model.UpdateAt; Content = model.Content; - if (model.MentionedUsers is not null) + if (model.MentionedUsers is { } users) { - ulong[] ids = model.MentionedUsers; - if (ids.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(ids.Length); - for (int i = 0; i < ids.Length; i++) - { - ulong id = ids[i]; - SocketUser user = Channel.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; - newMentions.Add(user ?? SocketUnknownUser.Create(Kook, state, id)); - } - - _userMentions = newMentions.ToImmutable(); - } + _userMentions = users.Select(x => + Channel.GetUserAsync(x, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser + ?? SocketUnknownUser.Create(Kook, state, x)) + .ToImmutableArray(); } } @@ -234,21 +221,12 @@ internal virtual void Update(ClientState state, MessageUpdateEvent model) EditedTimestamp = model.UpdatedAt; Content = model.Content; - if (model.Mention is not null) + if (model.MentionedUsers is { } users) { - ulong[] ids = model.Mention; - if (ids.Length > 0) - { - ImmutableArray.Builder newMentions = ImmutableArray.CreateBuilder(ids.Length); - for (int i = 0; i < ids.Length; i++) - { - ulong id = ids[i]; - SocketUser user = Channel.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; - newMentions.Add(user ?? SocketUnknownUser.Create(Kook, state, id)); - } - - _userMentions = newMentions.ToImmutable(); - } + _userMentions = users.Select(x => + Channel.GetUserAsync(x, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser + ?? SocketUnknownUser.Create(Kook, state, x)) + .ToImmutableArray(); } } @@ -259,58 +237,60 @@ internal virtual void Update(ClientState state, DirectMessageUpdateEvent model) } /// - public Task DeleteAsync(RequestOptions options = null) - => MessageHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + MessageHelper.DeleteAsync(this, Kook, options); internal void AddReaction(SocketReaction reaction) => _reactions.Add(reaction); internal void RemoveReaction(SocketReaction reaction) { - if (_reactions.Contains(reaction)) _reactions.Remove(reaction); + if (_reactions.Contains(reaction)) + _reactions.Remove(reaction); } internal void ClearReactions() => _reactions.Clear(); - internal void RemoveReactionsForEmote(IEmote emote) => _reactions.RemoveAll(x => x.Emote.Equals(emote)); + internal void RemoveReactionsForEmote(IEmote emote) => + _reactions.RemoveAll(x => x.Emote.Equals(emote)); /// - public Task UpdateAsync(RequestOptions options = null) - => SocketMessageHelper.UpdateAsync(this, Kook, options); + public Task UpdateAsync(RequestOptions? options = null) => + SocketMessageHelper.UpdateAsync(this, Kook, options); /// - public Task AddReactionAsync(IEmote emote, RequestOptions options = null) => + public Task AddReactionAsync(IEmote emote, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.AddReactionAsync(this, emote, Kook, options), IDMChannel => MessageHelper.AddDirectMessageReactionAsync(this, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) => + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Kook, options), IDMChannel => MessageHelper.RemoveDirectMessageReactionAsync(this, user.Id, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) => + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.RemoveReactionAsync(this, userId, emote, Kook, options), IDMChannel => MessageHelper.RemoveDirectMessageReactionAsync(this, userId, emote, Kook, options), - _ => Task.CompletedTask + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; /// - public Task> GetReactionUsersAsync(IEmote emote, RequestOptions options = null) => + public Task> GetReactionUsersAsync(IEmote emote, RequestOptions? options = null) => Channel switch { ITextChannel => MessageHelper.GetReactionUsersAsync(this, emote, Kook, options), IDMChannel => MessageHelper.GetDirectMessageReactionUsersAsync(this, emote, Kook, options), - _ => Task.FromResult>(null) + _ => throw new NotSupportedException("The operation is not supported for this message type.") }; #endregion @@ -323,7 +303,7 @@ public Task> GetReactionUsersAsync(IEmote emote, Requ /// public override string ToString() => Content; - internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; + internal SocketMessage Clone() => (SocketMessage)MemberwiseClone(); #region IMessage @@ -334,10 +314,10 @@ public Task> GetReactionUsersAsync(IEmote emote, Requ IMessageChannel IMessage.Channel => Channel; /// - IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IMessage.MentionedRoleIds => [..MentionedRoles.Select(x => x.Id)]; /// - IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IMessage.MentionedUserIds => [..MentionedUsers.Select(x => x.Id)]; /// IReadOnlyCollection IMessage.Attachments => Attachments; @@ -351,5 +331,8 @@ public Task> GetReactionUsersAsync(IEmote emote, Requ /// IReadOnlyCollection IMessage.Pokes => Pokes; + /// + bool? IMessage.IsPinned => IsPinned; + #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketMessageHelper.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketMessageHelper.cs index 4829b250..683bfc07 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketMessageHelper.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketMessageHelper.cs @@ -1,62 +1,49 @@ +using Kook.API; using Kook.API.Gateway; +using Kook.Rest; namespace Kook.WebSocket; internal static class SocketMessageHelper { - public static MessageSource GetSource(API.Message msg) - { - if (msg.Author.Bot ?? false) return MessageSource.Bot; - - if (msg.Author.IsSystemUser ?? msg.Author.Id == KookConfig.SystemMessageAuthorID) - return MessageSource.System; - - return MessageSource.User; - } - - public static MessageSource GetSource(API.DirectMessage msg, SocketUser user) - { - if (user.IsBot ?? false) return MessageSource.Bot; - - if (user.IsSystemUser ?? msg.AuthorId == KookConfig.SystemMessageAuthorID) - return MessageSource.System; - - return MessageSource.User; - } - public static MessageSource GetSource(GatewayGroupMessageExtraData msg) { - if (msg.Author.Bot ?? false) return MessageSource.Bot; - if (msg.Author.IsSystemUser ?? msg.Author.Id == KookConfig.SystemMessageAuthorID) return MessageSource.System; - + if (msg.Author.Bot is true) + return MessageSource.Bot; return MessageSource.User; } public static MessageSource GetSource(GatewayPersonMessageExtraData msg) { - if (msg.Author.Bot ?? false) return MessageSource.Bot; - if (msg.Author.IsSystemUser ?? msg.Author.Id == KookConfig.SystemMessageAuthorID) return MessageSource.System; - + if (msg.Author.Bot is true) + return MessageSource.Bot; return MessageSource.User; } - public static async Task UpdateAsync(SocketMessage msg, KookSocketClient client, RequestOptions options) + public static async Task UpdateAsync(SocketMessage msg, KookSocketClient client, RequestOptions? options) { switch (msg.Channel) { - case SocketTextChannel channel: - msg.Update(client.State, await client.ApiClient.GetMessageAsync(msg.Id, options).ConfigureAwait(false)); + case SocketTextChannel: + { + Message model = await client.ApiClient.GetMessageAsync(msg.Id, options).ConfigureAwait(false); + msg.Update(client.State, model); + } break; case SocketDMChannel channel: - msg.Update(client.State, - await client.ApiClient.GetDirectMessageAsync(msg.Id, channel.ChatCode, options: options).ConfigureAwait(false)); + { + DirectMessage model = await client.ApiClient + .GetDirectMessageAsync(msg.Id, channel.ChatCode, options) + .ConfigureAwait(false); + msg.Update(client.State, model); + } break; default: - throw new InvalidOperationException("Cannot reload a message from a non-SocketTextChannel or non-SocketTextChannel."); + throw new InvalidOperationException("Cannot reload a message from a channel type that is not a text channel or DM channel."); } } } diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketPokeAction.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketPokeAction.cs index 27574722..7bf7566f 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketPokeAction.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketPokeAction.cs @@ -19,11 +19,12 @@ public class SocketPokeAction : IPokeAction internal SocketPokeAction(SocketUser @operator, IEnumerable targets, Poke poke) { Operator = @operator; - Targets = targets as IReadOnlyCollection; + Targets = [..targets]; Poke = poke; } - internal static SocketPokeAction Create(KookSocketClient kook, SocketUser @operator, IEnumerable targets, API.Poke poke) + internal static SocketPokeAction Create(KookSocketClient kook, + SocketUser @operator, IEnumerable targets, API.Poke poke) { Poke restPoke = Poke.Create(poke); return new SocketPokeAction(@operator, targets, restPoke); diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketReaction.cs index 5bb9dbc9..ce118cb2 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using Kook.API.Gateway; + namespace Kook.WebSocket; /// @@ -36,7 +39,7 @@ public class SocketReaction : IReaction /// /// A user object where possible; a value is not always returned. /// - public IUser User { get; } + public IUser? User { get; internal set; } /// /// Gets the ID of the message that has been reacted to. @@ -52,7 +55,7 @@ public class SocketReaction : IReaction /// /// A WebSocket-based message where possible; a value is not always returned. /// - public SocketUserMessage Message { get; } + public IMessage? Message { get; internal set; } /// /// Gets the channel where the reaction takes place in. @@ -60,12 +63,13 @@ public class SocketReaction : IReaction /// /// A WebSocket-based message channel. /// - public ISocketMessageChannel Channel { get; } + public ISocketMessageChannel? Channel { get; } /// public IEmote Emote { get; } - internal SocketReaction(ISocketMessageChannel channel, Guid messageId, SocketUserMessage message, ulong userId, IUser user, IEmote emoji) + internal SocketReaction(ISocketMessageChannel? channel, Guid messageId, + IMessage? message, ulong userId, IUser? user, IEmote emoji) { Channel = channel; MessageId = messageId; @@ -75,40 +79,33 @@ internal SocketReaction(ISocketMessageChannel channel, Guid messageId, SocketUse Emote = emoji; } - internal static SocketReaction Create(API.Gateway.Reaction model, ISocketMessageChannel channel, SocketUserMessage message, IUser user) + internal static SocketReaction Create(Reaction model, ISocketMessageChannel? channel, + IMessage? message, IUser? user) { - IEmote emote; - if (Emoji.TryParse(model.Emoji.Id, out Emoji emoji)) - emote = emoji; - else - emote = new Emote(model.Emoji.Id, model.Emoji.Name); - + IEmote emote = Emoji.TryParse(model.Emoji.Id, out Emoji? emoji) + ? emoji + : new Emote(model.Emoji.Id, model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } - internal static SocketReaction Create(API.Gateway.PrivateReaction model, IDMChannel channel, SocketUserMessage message, IUser user) + internal static SocketReaction Create(PrivateReaction model, ISocketMessageChannel? channel, + IMessage? message, IUser? user) { - IEmote emote; - if (Emoji.TryParse(model.Emoji.Id, out Emoji emoji)) - emote = emoji; - else - emote = new Emote(model.Emoji.Id, model.Emoji.Name); - - return new SocketReaction(channel as ISocketMessageChannel, model.MessageId, message, model.UserId, user, emote); + IEmote emote = Emoji.TryParse(model.Emoji.Id, out Emoji? emoji) + ? emoji + : new Emote(model.Emoji.Id, model.Emoji.Name); + return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } /// - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { - if (obj == null) - return false; - if (obj == this) - return true; - - if (obj is not SocketReaction otherReaction) - return false; - - return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); + if (obj == null) return false; + if (obj == this) return true; + if (obj is not SocketReaction otherReaction) return false; + return UserId == otherReaction.UserId + && MessageId == otherReaction.MessageId + && Emote.Equals(otherReaction.Emote); } /// @@ -116,7 +113,7 @@ public override int GetHashCode() { unchecked { - var hashCode = UserId.GetHashCode(); + int hashCode = UserId.GetHashCode(); hashCode = (hashCode * 397) ^ MessageId.GetHashCode(); hashCode = (hashCode * 397) ^ Emote.GetHashCode(); return hashCode; diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index b172c39b..e0c44065 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -6,7 +6,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based message sent by the system. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { /// @@ -17,44 +17,44 @@ internal SocketSystemMessage(KookSocketClient kook, Guid id, ISocketMessageChann { } - internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) + internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, + ISocketMessageChannel channel, GatewayEvent gatewayEvent) { SocketSystemMessage entity = new(kook, gatewayEvent.MessageId, channel, author); - entity.Update(state, model, gatewayEvent); + entity.Update(state, gatewayEvent); return entity; } - internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) + internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, + ISocketMessageChannel channel, GatewayEvent gatewayEvent) { SocketSystemMessage entity = new(kook, gatewayEvent.MessageId, channel, author); - entity.Update(state, model, gatewayEvent); + entity.Update(state, gatewayEvent); return entity; } - internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.Message model) + internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, + ISocketMessageChannel channel, API.Message model) { SocketSystemMessage entity = new(kook, model.Id, channel, author); entity.Update(state, model); return entity; } - internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.DirectMessage model) + internal static new SocketSystemMessage Create(KookSocketClient kook, ClientState state, SocketUser author, + ISocketMessageChannel channel, API.DirectMessage model) { SocketSystemMessage entity = new(kook, model.Id, channel, author); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) => - base.Update(state, model, gatewayEvent); + internal override void Update(ClientState state, GatewayEvent gatewayEvent) => + base.Update(state, gatewayEvent); // TODO: SystemMessageType - internal override void Update(ClientState state, GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) => - base.Update(state, model, gatewayEvent); + internal override void Update(ClientState state, GatewayEvent gatewayEvent) => + base.Update(state, gatewayEvent); // TODO: SystemMessageType internal override void Update(ClientState state, API.Message model) => base.Update(state, model); @@ -64,5 +64,6 @@ internal override void Update(ClientState state, GatewayPersonMessageExtraData m // TODO: SystemMessageType private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; - internal new SocketSystemMessage Clone() => MemberwiseClone() as SocketSystemMessage; + + internal new SocketSystemMessage Clone() => (SocketSystemMessage)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 95e648c7..dc9a92ba 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -8,22 +8,21 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based message sent by a user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketUserMessage : SocketMessage, IUserMessage { - private bool? _isMentioningEveryone; - private bool? _isMentioningHere; - private Quote _quote; - private ImmutableArray _attachments = ImmutableArray.Create(); - private ImmutableArray _cards = ImmutableArray.Create(); - private ImmutableArray? _embeds; - private ImmutableArray _pokes; - private ImmutableArray _roleMentions = ImmutableArray.Create(); - private ImmutableArray _channelMentions = ImmutableArray.Create(); - private ImmutableArray _tags = ImmutableArray.Create(); - - /// - public Quote Quote => _quote; + private bool _isMentioningEveryone; + private bool _isMentioningHere; + private ImmutableArray _attachments = []; + private ImmutableArray _cards = []; + private ImmutableArray _embeds = []; + private ImmutableArray _pokes = []; + private ImmutableArray _roleMentions = []; + private ImmutableArray _channelMentions = []; + private ImmutableArray _tags = []; + + /// + public IQuote? Quote { get; private set; } /// /// Gets the that the message was sent from. @@ -31,10 +30,10 @@ public class SocketUserMessage : SocketMessage, IUserMessage /// /// The that the message was sent from. /// - public SocketGuild Guild { get; private set; } + public SocketGuild? Guild { get; private set; } - /// - public new bool? IsPinned { get; internal set; } + /// + public override bool IsPinned { get; protected internal set; } /// public override IReadOnlyCollection Attachments => _attachments; @@ -57,135 +56,184 @@ public class SocketUserMessage : SocketMessage, IUserMessage public IReadOnlyCollection MentionedChannels => _channelMentions; /// - public override bool? MentionedEveryone => _isMentioningEveryone; + public override bool MentionedEveryone => _isMentioningEveryone; /// - public override bool? MentionedHere => _isMentioningHere; + public override bool MentionedHere => _isMentioningHere; /// public override IReadOnlyCollection Tags => _tags; - internal SocketUserMessage(KookSocketClient kook, Guid id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + internal SocketUserMessage(KookSocketClient kook, Guid id, + ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(kook, id, channel, author, source) { } - internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) + internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, + GatewayEvent gatewayEvent) { - SocketUserMessage entity = new(kook, gatewayEvent.MessageId, channel, author, SocketMessageHelper.GetSource(model)); - entity.Update(state, model, gatewayEvent); + MessageSource messageSource = SocketMessageHelper.GetSource(gatewayEvent.ExtraData); + SocketUserMessage entity = new(kook, gatewayEvent.MessageId, channel, author, messageSource); + entity.Update(state, gatewayEvent); return entity; } - internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) + internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, + GatewayEvent gatewayEvent) { - SocketUserMessage entity = new(kook, gatewayEvent.MessageId, channel, author, SocketMessageHelper.GetSource(model)); - entity.Update(state, model, gatewayEvent); + MessageSource messageSource = SocketMessageHelper.GetSource(gatewayEvent.ExtraData); + SocketUserMessage entity = new(kook, gatewayEvent.MessageId, channel, author, messageSource); + entity.Update(state, gatewayEvent); return entity; } - internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.Message model) + internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, API.Message model) { - SocketUserMessage entity = new(kook, model.Id, channel, author, SocketMessageHelper.GetSource(model)); + MessageSource messageSource = MessageHelper.GetSource(model); + SocketUserMessage entity = new(kook, model.Id, channel, author, messageSource); entity.Update(state, model); return entity; } - internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, SocketUser author, ISocketMessageChannel channel, - API.DirectMessage model) + internal static new SocketUserMessage Create(KookSocketClient kook, ClientState state, + SocketUser author, ISocketMessageChannel channel, API.DirectMessage model) { - SocketUserMessage entity = new(kook, model.Id, channel, author, SocketMessageHelper.GetSource(model, author)); + MessageSource messageSource = MessageHelper.GetSource(author); + SocketUserMessage entity = new(kook, model.Id, channel, author, messageSource); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, GatewayGroupMessageExtraData model, GatewayEvent gatewayEvent) + internal override void Update(ClientState state, GatewayEvent gatewayEvent) { - base.Update(state, model, gatewayEvent); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + base.Update(state, gatewayEvent); + + GatewayGroupMessageExtraData model = gatewayEvent.ExtraData; + Content = gatewayEvent.Content; + RawContent = model.KMarkdownInfo?.RawContent ?? gatewayEvent.Content; _isMentioningEveryone = model.MentionedAll; _isMentioningHere = model.MentionedHere; - _roleMentions = model.MentionedRoles?.Select(x => guild?.GetRole(x)).ToImmutableArray() ?? new ImmutableArray(); - _channelMentions = model.MentionedChannels?.Select(x => guild?.GetChannel(x)).ToImmutableArray() ?? new ImmutableArray(); - Content = gatewayEvent.Content; - RawContent = model.KMarkdownInfo?.RawContent; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(gatewayEvent.Content, Channel, guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) - _tags = MessageHelper.ParseTags(gatewayEvent.Content, Channel, guild, MentionedUsers, TagMode.KMarkdown); - if (model.Quote is not null) - _quote = Quote.Create(model.Quote.Id, model.Quote.QuotedMessageId, model.Quote.Type, model.Quote.Content, - model.Quote.CreateAt, guild?.GetUser(model.Quote.Author.Id)); + Guild = (Channel as SocketGuildChannel)?.Guild; + if (Guild is not null) + { + if (model.MentionedRoles is { } roles) + _roleMentions = [..roles.Select(x => Guild.GetRole(x) ?? new SocketRole(Guild, x))]; + if (model.MentionedChannels is { } channels) + _channelMentions = [..channels.Select(x => Guild.GetChannel(x) ?? new SocketGuildChannel(Kook, x, Guild))]; + } + + _tags = model.Type switch + { + MessageType.Text => MessageHelper.ParseTags(gatewayEvent.Content, Channel, Guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(gatewayEvent.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; - if (model.Attachment is not null) _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } + + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { _cards = MessageHelper.ParseCards(gatewayEvent.Content); - _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; } - _pokes = Type == MessageType.Poke && model.KMarkdownInfo?.Pokes is not null - ? model.KMarkdownInfo.Pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x)).ToImmutableArray() - : ImmutableArray.Empty; + if (Type == MessageType.Poke && model.KMarkdownInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; IsPinned = false; - Guild = guild; } - internal override void Update(ClientState state, GatewayPersonMessageExtraData model, GatewayEvent gatewayEvent) + internal override void Update(ClientState state, GatewayEvent gatewayEvent) { - base.Update(state, model, gatewayEvent); + base.Update(state, gatewayEvent); + + GatewayPersonMessageExtraData model = gatewayEvent.ExtraData; Content = gatewayEvent.Content; - RawContent = model.KMarkdownInfo?.RawContent; - if (model.Quote is not null) - _quote = Quote.Create(model.Quote.Id, - model.Quote.QuotedMessageId, model.Quote.Type, model.Quote.Content, model.Quote.CreateAt, - state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author))); + RawContent = model.KMarkdownInfo.RawContent ?? gatewayEvent.Content; + _attachments = []; + + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } - if (model.Attachment is not null) _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { _cards = MessageHelper.ParseCards(gatewayEvent.Content); - _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; } - if (Type == MessageType.Poke && model.KMarkdownInfo?.Pokes is not null) - { - SocketUser recipient = (Channel as SocketDMChannel)?.Recipient; - SocketUser target = recipient is null - ? null - : recipient.Id == Author.Id - ? Kook.CurrentUser - : recipient; - _pokes = model.KMarkdownInfo.Pokes.Select(x => SocketPokeAction.Create(Kook, Author, - new[] { target }, x)).ToImmutableArray(); - } - else - _pokes = ImmutableArray.Empty; + if (Type == MessageType.Poke && model.KMarkdownInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; } internal override void Update(ClientState state, API.Message model) { base.Update(state, model); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + + Content = model.Content; _isMentioningEveryone = model.MentionedAll; _isMentioningHere = model.MentionedHere; - _roleMentions = model.MentionedRoles?.Select(x => guild.GetRole(x)).ToImmutableArray() ?? new ImmutableArray(); - _channelMentions = model.MentionInfo?.MentionedChannels?.Select(x => guild.GetChannel(x.Id)).ToImmutableArray() - ?? new ImmutableArray(); - Content = model.Content; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown); + _attachments = []; - if (model.Attachment is not null) _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + Guild = (Channel as SocketGuildChannel)?.Guild; + if (Guild is not null) + { + _roleMentions = [..model.MentionedRoles.Select(x => Guild.GetRole(x) ?? new SocketRole(Guild, x))]; + if (model.MentionInfo?.MentionedChannels is { } channels) + _channelMentions = [..channels.Select(x => Guild.GetChannel(x.Id) ?? new SocketGuildChannel(Kook, x.Id, Guild))]; + } + + _tags = model.Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; + + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } + + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { @@ -193,25 +241,40 @@ internal override void Update(ClientState state, API.Message model) _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); } - _embeds = model.Embeds.Select(x => x.ToEntity()).ToImmutableArray(); - _pokes = Type == MessageType.Poke && model.MentionInfo?.Pokes is not null - ? model.MentionInfo.Pokes.Select(x => SocketPokeAction.Create(Kook, Author, - model.MentionedUsers.Select(state.GetUser), x)).ToImmutableArray() - : ImmutableArray.Empty; + _embeds = [..model.Embeds.Select(x => x.ToEntity())]; - Guild = guild; + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; } internal override void Update(ClientState state, API.DirectMessage model) { base.Update(state, model); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + Content = model.Content; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown); + _attachments = []; + + _tags = model.Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; - if (model.Attachment is not null) _attachments = _attachments.Add(Attachment.Create(model.Attachment)); + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } + + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; if (Type == MessageType.Card) { @@ -219,65 +282,103 @@ internal override void Update(ClientState state, API.DirectMessage model) _attachments = _attachments.AddRange(MessageHelper.ParseAttachments(_cards.OfType())); } - _embeds = model.Embeds.Select(x => x.ToEntity()).ToImmutableArray(); - if (Type == MessageType.Poke && model.MentionInfo?.Pokes is not null) - { - SocketUser recipient = (Channel as SocketDMChannel)?.Recipient; - SocketUser target = recipient is null - ? null - : recipient.Id == Author.Id - ? Kook.CurrentUser - : recipient; - _pokes = model.MentionInfo.Pokes.Select(x => SocketPokeAction.Create(Kook, Author, - new[] { target }, x)).ToImmutableArray(); - } - else - _pokes = ImmutableArray.Empty; + _embeds = [..model.Embeds.Select(x => x.ToEntity())]; - Guild = guild; + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; } internal override void Update(ClientState state, MessageUpdateEvent model) { base.Update(state, model); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; - _isMentioningEveryone = model.MentionAll; - _isMentioningHere = model.MentionHere; - _roleMentions = model.MentionRoles?.Select(x => Guild.GetRole(x)).ToImmutableArray() - ?? new ImmutableArray(); + Content = model.Content; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown); + _isMentioningEveryone = model.MentionedAll; + _isMentioningHere = model.MentionedHere; + _attachments = []; + + Guild = (Channel as SocketGuildChannel)?.Guild; + if (Guild is not null) + { + _roleMentions = [..model.MentionedRoles.Select(x => Guild.GetRole(x) ?? new SocketRole(Guild, x))]; + _channelMentions = [..model.MentionInfo.MentionedChannels.Select(x => Guild.GetChannel(x.Id) ?? new SocketGuildChannel(Kook, x.Id, Guild))]; + } - _cards = Type == MessageType.Card - ? MessageHelper.ParseCards(model.Content) - : ImmutableArray.Create(); + _tags = Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; - Guild = guild; + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } + + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; + + if (Type == MessageType.Card) + { + _cards = MessageHelper.ParseCards(model.Content); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; + } + + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; } internal override void Update(ClientState state, DirectMessageUpdateEvent model) { base.Update(state, model); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; Content = model.Content; - if (Type == MessageType.Text) - _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText); - else if (Type == MessageType.KMarkdown) _tags = MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown); + _attachments = []; - _cards = Type == MessageType.Card - ? MessageHelper.ParseCards(model.Content) - : ImmutableArray.Create(); + _tags = Type switch + { + MessageType.Text => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.PlainText), + MessageType.KMarkdown => MessageHelper.ParseTags(model.Content, Channel, Guild, MentionedUsers, TagMode.KMarkdown), + _ => _tags + }; - Guild = guild; + if (model.Quote is { } quote) + { + IUser author = Guild?.GetUser(model.Quote.Author.Id) + ?? Guild?.AddOrUpdateUser(model.Quote.Author) + ?? state.GetOrAddUser(model.Quote.Author.Id, _ => SocketGlobalUser.Create(Kook, state, model.Quote.Author)) as IUser; + Guid? quotedMessageId = quote.RongId ?? quote.QuotedMessageId; + if (quotedMessageId == Guid.Empty) + Quote = null; + else if (quotedMessageId.HasValue) + Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, author); + } + + if (model.Attachment is { } attachment) + _attachments = [.._attachments, Attachment.Create(attachment)]; + + if (Type == MessageType.Card) + { + _cards = MessageHelper.ParseCards(model.Content); + _attachments = [.._attachments, ..MessageHelper.ParseAttachments(_cards.OfType())]; + } + + if (Type == MessageType.Poke && model.MentionInfo is { Pokes: { } pokes }) + _pokes = [..pokes.Select(x => SocketPokeAction.Create(Kook, Author, MentionedUsers, x))]; } /// /// Only the author of a message may modify the message. /// Message content is too long, length must be less or equal to . - public Task ModifyAsync(Action func, RequestOptions options = null) - => MessageHelper.ModifyAsync(this, Kook, func, options); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + MessageHelper.ModifyAsync(this, Kook, func, options); /// /// Transforms this message's text into a human-readable form by resolving its tags. @@ -288,28 +389,34 @@ public Task ModifyAsync(Action func, RequestOptions options = /// Determines how the role tag should be handled. /// Determines how the @everyone tag should be handled. /// Determines how the emoji tag should be handled. - public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, - TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Name, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, + TagHandling everyoneHandling = TagHandling.Name, TagHandling emojiHandling = TagHandling.Name) => + MentionUtils.Resolve(this, startIndex, + userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); /// - public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, - TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Name, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - - private string DebuggerDisplay => - $"{Author}: {Content} ({Id}{(Attachments is not null && Attachments.Any() ? $", {Attachments.Count} Attachment{(Attachments.Count == 1 ? string.Empty : "s")}" : string.Empty)})"; + public string Resolve(TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, + TagHandling everyoneHandling = TagHandling.Name, TagHandling emojiHandling = TagHandling.Name) => + MentionUtils.Resolve(this, 0, + userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{ + Attachments.Count switch + { + 0 => string.Empty, + 1 => ", 1 Attachment", + _ => $", {Attachments.Count} Attachments" + }})"; - internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; + internal new SocketUserMessage Clone() => (SocketUserMessage)MemberwiseClone(); #region IUserMessage /// bool? IMessage.IsPinned => IsPinned; - /// - IQuote IUserMessage.Quote => _quote; - /// IReadOnlyCollection IMessage.Cards => Cards; diff --git a/src/Kook.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Kook.Net.WebSocket/Entities/Roles/SocketRole.cs index ae7b22aa..fc88dd9f 100644 --- a/src/Kook.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Kook.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -9,7 +9,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based role to be given to a guild user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketRole : SocketEntity, IRole { #region SocketRole @@ -23,7 +23,7 @@ public class SocketRole : SocketEntity, IRole public SocketGuild Guild { get; } /// - public RoleType? Type { get; private set; } + public RoleType Type { get; private set; } /// public string Name { get; private set; } @@ -64,8 +64,11 @@ public class SocketRole : SocketEntity, IRole public string PlainTextMention => IsEveryone ? "@全体成员" : MentionUtils.PlainTextMentionRole(Id); internal SocketRole(SocketGuild guild, uint id) - : base(guild.Kook, id) => + : base(guild.Kook, id) + { + Name = string.Empty; Guild = guild; + } internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) { @@ -88,12 +91,12 @@ internal void Update(ClientState state, Model model) } /// - public Task ModifyAsync(Action func, RequestOptions options = null) - => RoleHelper.ModifyAsync(this, Kook, func, options); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + RoleHelper.ModifyAsync(this, Kook, func, options); /// - public Task DeleteAsync(RequestOptions options = null) - => RoleHelper.DeleteAsync(this, Kook, options); + public Task DeleteAsync(RequestOptions? options = null) => + RoleHelper.DeleteAsync(this, Kook, options); /// /// Gets a collection of users with this role. @@ -108,36 +111,30 @@ public Task DeleteAsync(RequestOptions options = null) /// the data via REST and update the guild users cache, otherwise it will /// return the cached data. /// - public async IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + public async IAsyncEnumerable> GetUsersAsync(RequestOptions? options = null) { // From SocketGuild.Users if (Guild.HasAllMembers is true) { IEnumerable> userCollections = Guild.Users .Where(u => u.Roles.Contains(this)) -#if NET6_0_OR_GREATER .Chunk(KookConfig.MaxUsersPerBatch) -#else - .Select((x, i) => new { Index = i, Value = x }) - .GroupBy(x => x.Index / KookConfig.MaxUsersPerBatch) - .Select(x => x.Select(v => v.Value)) -#endif - .Select(c => c.ToImmutableArray() as IReadOnlyCollection); - foreach (IReadOnlyCollection users in userCollections) yield return users; - + .Select(c => c.ToImmutableList()); + foreach (IReadOnlyCollection users in userCollections) + yield return users; yield break; } // Update SocketGuild.Users by fetching from REST API - void Func(SearchGuildMemberProperties p) => p.RoleId = Id; - IAsyncEnumerable> restUserCollections = Kook.ApiClient - .GetGuildMembersAsync(Guild.Id, Func, KookConfig.MaxUsersPerBatch, 1, options); + IAsyncEnumerable> restUserCollections = Kook + .ApiClient + .GetGuildMembersAsync(Guild.Id, x => x.RoleId = Id, KookConfig.MaxUsersPerBatch, 1, options); await foreach (IReadOnlyCollection restUsers in restUserCollections) - yield return restUsers.Select(restUser => Guild.AddOrUpdateUser(restUser)).ToList(); + yield return [..restUsers.Select(restUser => Guild.AddOrUpdateUser(restUser))]; } /// - public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + public int CompareTo(IRole? role) => RoleUtils.Compare(this, role); #endregion @@ -150,7 +147,7 @@ public async IAsyncEnumerable> GetUsersAsyn public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; - internal SocketRole Clone() => MemberwiseClone() as SocketRole; + internal SocketRole Clone() => (SocketRole)MemberwiseClone(); #region IRole @@ -158,13 +155,10 @@ public async IAsyncEnumerable> GetUsersAsyn IGuild IRole.Guild => Guild; /// - IAsyncEnumerable> IRole.GetUsersAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); - } + IAsyncEnumerable> IRole.GetUsersAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 5191d497..5ec1d653 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -3,14 +3,17 @@ namespace Kook.WebSocket; -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -internal class SocketGlobalUser : SocketUser +[DebuggerDisplay("{DebuggerDisplay,nq}")] +internal sealed class SocketGlobalUser : SocketUser { + private readonly object _lockObj = new(); + private ushort _references; + /// public override string Username { get; internal set; } /// - public override ushort? IdentifyNumberValue { get; internal set; } + public override ushort IdentifyNumberValue { get; internal set; } /// public override bool? IsBot { get; internal set; } @@ -31,32 +34,32 @@ internal class SocketGlobalUser : SocketUser public override string BuffAvatar { get; internal set; } /// - public override string Banner { get; internal set; } + public override string? Banner { get; internal set; } /// public override bool? IsDenoiseEnabled { get; internal set; } /// - public override UserTag UserTag { get; internal set; } + public override UserTag? UserTag { get; internal set; } /// public override IReadOnlyCollection Nameplates { get; internal set; } - /// - public override bool? IsSystemUser { get; internal set; } - /// internal override SocketPresence Presence { get; set; } /// internal override SocketGlobalUser GlobalUser => this; - private readonly object _lockObj = new(); - private ushort _references; - public SocketGlobalUser(KookSocketClient kook, ulong id) : base(kook, id) { + Username = string.Empty; + Avatar = string.Empty; + BuffAvatar = string.Empty; + Banner = string.Empty; + Nameplates = []; + Presence = new SocketPresence(); } internal static SocketGlobalUser Create(KookSocketClient kook, ClientState state, Model model) @@ -82,10 +85,13 @@ internal void RemoveRef(KookSocketClient kook) { lock (_lockObj) { - if (--_references <= 0) kook.RemoveUser(Id); + if (--_references <= 0) + kook.RemoveUser(Id); } } - private string DebuggerDisplay => $"{Username}#{IdentifyNumber} ({Id}{(IsBot ?? false ? ", Bot" : "")}, Global)"; - internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; + private string DebuggerDisplay => + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")}, Global)"; + internal new SocketGlobalUser Clone() => (SocketGlobalUser)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 4080a8bd..e3df5940 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -11,7 +11,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based guild user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketGuildUser : SocketUser, IGuildUser, IUpdateable { #region SocketGuildUser @@ -29,19 +29,19 @@ public class SocketGuildUser : SocketUser, IGuildUser, IUpdateable public string DisplayName => Nickname ?? Username; /// - public string Nickname { get; private set; } + public string? Nickname { get; private set; } /// - public bool IsMobileVerified { get; private set; } + public bool? IsMobileVerified { get; private set; } /// - public DateTimeOffset JoinedAt { get; private set; } + public DateTimeOffset? JoinedAt { get; private set; } /// - public DateTimeOffset ActiveAt { get; private set; } + public DateTimeOffset? ActiveAt { get; private set; } /// - public Color Color { get; private set; } + public Color? Color { get; private set; } /// public bool? IsOwner { get; private set; } @@ -64,7 +64,7 @@ public override string Username } /// - public override ushort? IdentifyNumberValue + public override ushort IdentifyNumberValue { get => GlobalUser.IdentifyNumberValue; internal set => GlobalUser.IdentifyNumberValue = value; @@ -85,7 +85,7 @@ public override string BuffAvatar } /// - public override string Banner + public override string? Banner { get => GlobalUser.Banner; internal set => GlobalUser.Banner = value; @@ -120,7 +120,7 @@ public override bool? IsDenoiseEnabled } /// - public override UserTag UserTag + public override UserTag? UserTag { get => GlobalUser.UserTag; internal set => GlobalUser.UserTag = value; @@ -133,24 +133,21 @@ public override IReadOnlyCollection Nameplates internal set => GlobalUser.Nameplates = value; } - /// - public override bool? IsSystemUser - { - get => GlobalUser.IsSystemUser; - internal set => GlobalUser.IsSystemUser = value; - } - /// public GuildPermissions GuildPermissions => new(Permissions.ResolveGuild(Guild, this)); /// - internal override SocketPresence Presence { get; set; } + internal override SocketPresence Presence + { + get => GlobalUser.Presence; + set => GlobalUser.Presence = value; + } /// - public bool? IsDeafened => VoiceState?.IsDeafened ?? false; + public bool? IsDeafened => VoiceState?.IsDeafened; /// - public bool? IsMuted => VoiceState?.IsMuted ?? false; + public bool? IsMuted => VoiceState?.IsMuted; /// /// Gets a collection of all boost subscriptions of this user for this guild. @@ -175,11 +172,11 @@ public override bool? IsSystemUser /// /// /// - public IReadOnlyCollection BoostSubscriptions => - Guild.BoostSubscriptions? + public IReadOnlyCollection BoostSubscriptions => Guild.BoostSubscriptions? .Where(x => x.Key.Id == Id) .SelectMany(x => x.Value) - .ToImmutableArray(); + .ToImmutableArray() + ?? []; /// /// Returns a collection of roles that the user possesses. @@ -191,8 +188,10 @@ public override bool? IsSystemUser /// it is recommended to call before this property is used. /// /// - public IReadOnlyCollection Roles - => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); + public IReadOnlyCollection Roles => _roleIds + .Select(x => Guild.GetRole(x) ?? new SocketRole(Guild, x)) + .Where(x => x != null) + .ToImmutableArray(); /// /// Returns the voice channel the user is in, or null if none or unknown. @@ -204,7 +203,7 @@ public IReadOnlyCollection Roles /// or . /// /// - public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; + public SocketVoiceChannel? VoiceChannel => VoiceState?.VoiceChannel; /// /// Gets the voice status of the user if any. @@ -218,6 +217,7 @@ public IReadOnlyCollection Roles internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) : base(guild.Kook, globalUser.Id) { + _roleIds = []; Guild = guild; GlobalUser = globalUser; } @@ -227,7 +227,6 @@ internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Use SocketGuildUser entity = new(guild, guild.Kook.GetOrCreateUser(state, model)); entity.Update(state, model); entity.UpdatePresence(model.Online, model.OperatingSystem); - entity.UpdateRoles(Array.Empty()); return entity; } @@ -241,6 +240,8 @@ internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Mem internal static SocketGuildUser Create(SocketGuild guild, ClientState state, RichGuild model) { + if (guild.Kook.CurrentUser is null) + throw new InvalidOperationException("The current user is not set well via login."); SocketGuildUser entity = new(guild, guild.Kook.CurrentUser.GlobalUser); entity.Update(state, model); return entity; @@ -257,49 +258,48 @@ internal void Update(ClientState state, MemberModel model) ActiveAt = model.ActiveAt; Color = model.Color; IsOwner = model.IsOwner; - UpdateRoles(model.Roles); + if (model.Roles is not null) + UpdateRoles(model.Roles); } internal void Update(ClientState state, RichGuild guildModel) { Nickname = guildModel.CurrentUserNickname == Username ? null : guildModel.CurrentUserNickname; - UpdateRoles(guildModel.CurrentUserRoles); + if (guildModel.CurrentUserRoles is not null) + UpdateRoles(guildModel.CurrentUserRoles); } - internal void Update(ClientState state, GuildMemberUpdateEvent model) => Nickname = model.Nickname; - - internal override void UpdatePresence(bool? isOnline) + /// + /// Updates the nickname of this user. + /// + internal void UpdateNickname() { - base.UpdatePresence(isOnline); - GlobalUser.UpdatePresence(isOnline); + if (Nickname == Username) + Nickname = null; } - internal override void UpdatePresence(bool? isOnline, string activeClient) + internal void Update(ClientState state, GuildMemberUpdateEvent model) { - base.UpdatePresence(isOnline, activeClient); - GlobalUser.UpdatePresence(isOnline, activeClient); + Nickname = model.Nickname == Username ? null : model.Nickname; } private void UpdateRoles(uint[] roleIds) { - ImmutableArray.Builder roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(0); - for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); - - _roleIds = roles.ToImmutable(); + _roleIds = [..roleIds]; } /// - public Task ModifyNicknameAsync(string name, RequestOptions options = null) - => UserHelper.ModifyNicknameAsync(this, Kook, name, options); + public Task ModifyNicknameAsync(string? name, RequestOptions? options = null) => + UserHelper.ModifyNicknameAsync(this, Kook, name, options); /// - public Task> GetBoostSubscriptionsAsync(RequestOptions options = null) - => UserHelper.GetBoostSubscriptionsAsync(this, Kook, options); + public Task> GetBoostSubscriptionsAsync( + RequestOptions? options = null) => + UserHelper.GetBoostSubscriptionsAsync(this, Kook, options); /// - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Kook, options); + public Task KickAsync(RequestOptions? options = null) => + UserHelper.KickAsync(this, Kook, options); /// /// @@ -307,8 +307,8 @@ public Task KickAsync(RequestOptions options = null) /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task AddRoleAsync(uint roleId, RequestOptions options = null) - => AddRolesAsync(new[] { roleId }, options); + public Task AddRoleAsync(uint roleId, RequestOptions? options = null) => + AddRolesAsync(new[] { roleId }, options); /// /// @@ -316,8 +316,8 @@ public Task AddRoleAsync(uint roleId, RequestOptions options = null) /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRoleAsync(role.Id, options); + public Task AddRoleAsync(IRole role, RequestOptions? options = null) => + AddRoleAsync(role.Id, options); /// /// @@ -325,8 +325,8 @@ public Task AddRoleAsync(IRole role, RequestOptions options = null) /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Kook, roleIds, options); + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions? options = null) => + UserHelper.AddRolesAsync(this, Kook, roleIds, options); /// /// @@ -334,8 +334,8 @@ public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = nu /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => AddRolesAsync(roles.Select(x => x.Id), options); + public Task AddRolesAsync(IEnumerable roles, RequestOptions? options = null) => + AddRolesAsync(roles.Select(x => x.Id), options); /// /// @@ -343,8 +343,8 @@ public Task AddRolesAsync(IEnumerable roles, RequestOptions options = nul /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task RemoveRoleAsync(uint roleId, RequestOptions options = null) - => RemoveRolesAsync(new[] { roleId }, options); + public Task RemoveRoleAsync(uint roleId, RequestOptions? options = null) => + RemoveRolesAsync(new[] { roleId }, options); /// /// @@ -352,8 +352,8 @@ public Task RemoveRoleAsync(uint roleId, RequestOptions options = null) /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRoleAsync(role.Id, options); + public Task RemoveRoleAsync(IRole role, RequestOptions? options = null) => + RemoveRoleAsync(role.Id, options); /// /// @@ -361,8 +361,8 @@ public Task RemoveRoleAsync(IRole role, RequestOptions options = null) /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Kook, roleIds, options); + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions? options = null) => + UserHelper.RemoveRolesAsync(this, Kook, roleIds, options); /// /// @@ -370,32 +370,32 @@ public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = /// the property will not be updated immediately after /// calling this method. To update the cached roles of this user, please use . /// - public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => RemoveRolesAsync(roles.Select(x => x.Id)); + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions? options = null) => + RemoveRolesAsync(roles.Select(x => x.Id)); /// - public Task MuteAsync(RequestOptions options = null) - => GuildHelper.MuteUserAsync(this, Kook, options); + public Task MuteAsync(RequestOptions? options = null) => + GuildHelper.MuteUserAsync(this, Kook, options); /// - public Task DeafenAsync(RequestOptions options = null) - => GuildHelper.DeafenUserAsync(this, Kook, options); + public Task DeafenAsync(RequestOptions? options = null) => + GuildHelper.DeafenUserAsync(this, Kook, options); /// - public Task UnmuteAsync(RequestOptions options = null) - => GuildHelper.UnmuteUserAsync(this, Kook, options); + public Task UnmuteAsync(RequestOptions? options = null) => + GuildHelper.UnmuteUserAsync(this, Kook, options); /// - public Task UndeafenAsync(RequestOptions options = null) - => GuildHelper.UndeafenUserAsync(this, Kook, options); + public Task UndeafenAsync(RequestOptions? options = null) => + GuildHelper.UndeafenUserAsync(this, Kook, options); /// - public async Task> GetConnectedVoiceChannelsAsync(RequestOptions options = null) + public async Task> GetConnectedVoiceChannelsAsync(RequestOptions? options = null) { IReadOnlyCollection channels = await SocketUserHelper.GetConnectedChannelsAsync(this, Kook, options).ConfigureAwait(false); - foreach (SocketVoiceChannel channel in channels) channel.Guild.AddOrUpdateVoiceState(Id, channel.Id); - + foreach (SocketVoiceChannel channel in channels) + channel.Guild.AddOrUpdateVoiceState(Id, channel.Id); return channels; } @@ -407,12 +407,12 @@ public async Task> GetConnectedVoiceChan /// /// A task that represents the asynchronous reloading operation. /// - public Task UpdateAsync(RequestOptions options = null) - => SocketUserHelper.UpdateAsync(this, Kook, options); + public Task UpdateAsync(RequestOptions? options = null) => + SocketUserHelper.UpdateAsync(this, Kook, options); /// - public ChannelPermissions GetPermissions(IGuildChannel channel) - => new(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + public ChannelPermissions GetPermissions(IGuildChannel channel) => + new(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); #endregion @@ -428,18 +428,21 @@ public ChannelPermissions GetPermissions(IGuildChannel channel) IReadOnlyCollection IGuildUser.RoleIds => _roleIds; /// - async Task> IGuildUser.GetConnectedVoiceChannelsAsync(RequestOptions options) - => await GetConnectedVoiceChannelsAsync(options).ConfigureAwait(false); + async Task> IGuildUser.GetConnectedVoiceChannelsAsync(RequestOptions? options) => + await GetConnectedVoiceChannelsAsync(options).ConfigureAwait(false); #endregion #region IVoiceState /// - IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + IVoiceChannel? IVoiceState.VoiceChannel => VoiceChannel; #endregion - private string DebuggerDisplay => $"{Username}#{IdentifyNumber} ({Id}{(IsBot ?? false ? ", Bot" : "")}, Guild)"; - internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + private string DebuggerDisplay => + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")}, Guild)"; + + internal new SocketGuildUser Clone() => (SocketGuildUser)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketPresence.cs index cf1a96a9..cc5a1094 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -5,7 +5,7 @@ namespace Kook.WebSocket; /// /// Represents the WebSocket user's presence status. This may include their online status and their activity. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketPresence : IPresence { /// @@ -18,12 +18,6 @@ internal SocketPresence() { } - internal SocketPresence(bool? isOnline, ClientType? activeClient) - { - IsOnline = isOnline; - ActiveClient = activeClient; - } - internal static SocketPresence Create(bool? isOnline, string activeClient) { SocketPresence entity = new(); @@ -33,15 +27,15 @@ internal static SocketPresence Create(bool? isOnline, string activeClient) internal void Update(bool? isOnline) { - if (isOnline.HasValue) IsOnline = isOnline; + if (isOnline.HasValue) + IsOnline = isOnline; } - internal void Update(bool? isOnline, string activeClient) + internal void Update(bool? isOnline, string? activeClient) { - if (isOnline.HasValue) IsOnline = isOnline; - - if (!string.IsNullOrWhiteSpace(activeClient)) - ActiveClient = ConvertClientType(activeClient); + if (isOnline.HasValue) + IsOnline = isOnline; + ActiveClient = ConvertClientType(activeClient); } /// @@ -53,16 +47,21 @@ internal void Update(bool? isOnline, string activeClient) /// /// A that this user is active. /// - private static ClientType? ConvertClientType(string clientType) + private static ClientType? ConvertClientType(string? clientType) { - if (string.IsNullOrWhiteSpace(clientType)) return null; - - if (Enum.TryParse(clientType, true, out ClientType type)) return type; - + if (string.IsNullOrWhiteSpace(clientType)) + return null; + if (Enum.TryParse(clientType, true, out ClientType type)) + return type; return null; } - private string DebuggerDisplay => $"{IsOnline switch { true => "Online", false => "Offline", _ => "Unknown Status" }}, {ActiveClient.ToString()}"; + private string DebuggerDisplay => $"{IsOnline switch + { + true => "Online", + false => "Offline", + _ => "Unknown Status" + }}, {ActiveClient.ToString()}"; - internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; + internal SocketPresence Clone() => (SocketPresence)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketSelfUser.cs index b9a505d6..881f5134 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -7,8 +7,8 @@ namespace Kook.WebSocket; /// /// Represents the logged-in WebSocket-based user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] -public class SocketSelfUser : SocketUser, ISelfUser +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class SocketSelfUser : SocketUser, ISelfUser, IUpdateable { internal override SocketGlobalUser GlobalUser { get; } @@ -27,7 +27,7 @@ public override string Username } /// - public override ushort? IdentifyNumberValue + public override ushort IdentifyNumberValue { get => GlobalUser.IdentifyNumberValue; internal set => GlobalUser.IdentifyNumberValue = value; @@ -48,7 +48,7 @@ public override string BuffAvatar } /// - public override string Banner + public override string? Banner { get => GlobalUser.Banner; internal set => GlobalUser.Banner = value; @@ -83,7 +83,7 @@ public override bool? IsDenoiseEnabled } /// - public override UserTag UserTag + public override UserTag? UserTag { get => GlobalUser.UserTag; internal set => GlobalUser.UserTag = value; @@ -96,13 +96,6 @@ public override IReadOnlyCollection Nameplates internal set => GlobalUser.Nameplates = value; } - /// - public override bool? IsSystemUser - { - get => GlobalUser.IsSystemUser; - internal set => GlobalUser.IsSystemUser = value; - } - /// internal override SocketPresence Presence { @@ -111,10 +104,10 @@ internal override SocketPresence Presence } /// - public string MobilePrefix { get; internal set; } + public string? MobilePrefix { get; internal set; } /// - public string Mobile { get; internal set; } + public string? Mobile { get; internal set; } /// public int InvitedCount { get; internal set; } @@ -123,8 +116,12 @@ internal override SocketPresence Presence public bool IsMobileVerified { get; internal set; } internal SocketSelfUser(KookSocketClient kook, SocketGlobalUser globalUser) - : base(kook, globalUser.Id) => + : base(kook, globalUser.Id) + { GlobalUser = globalUser; + MobilePrefix = string.Empty; + Mobile = string.Empty; + } internal static SocketSelfUser Create(KookSocketClient kook, ClientState state, Model model) { @@ -136,51 +133,44 @@ internal static SocketSelfUser Create(KookSocketClient kook, ClientState state, internal bool Update(ClientState state, Model model) { - bool hasGlobalChanges = base.Update(state, model); + bool hasChanged = base.Update(state, model); UpdatePresence(model.Online, model.OperatingSystem); - if (model.MobilePrefix != MobilePrefix) - { - MobilePrefix = model.MobilePrefix; - hasGlobalChanges = true; - } - - if (model.Mobile != Mobile) - { - Mobile = model.Mobile; - hasGlobalChanges = true; - } - - if (model.InvitedCount != InvitedCount) - { - InvitedCount = model.InvitedCount ?? 0; - hasGlobalChanges = true; - } - - if (model.MobileVerified != IsMobileVerified) - { - IsMobileVerified = model.MobileVerified; - hasGlobalChanges = true; - } - - return hasGlobalChanges; - } - - private string DebuggerDisplay => $"{Username}#{IdentifyNumber} ({Id}{(IsBot ?? false ? ", Bot" : "")}, Self)"; - internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + hasChanged |= ValueHelper.SetIfChanged(() => MobilePrefix, x => MobilePrefix = x, model.MobilePrefix); + hasChanged |= ValueHelper.SetIfChanged(() => Mobile, x => Mobile = x, model.Mobile); + hasChanged |= ValueHelper.SetIfChanged(() => IsMobileVerified, x => IsMobileVerified = x, model.MobileVerified); + hasChanged |= ValueHelper.SetIfChanged(() => InvitedCount, x => InvitedCount = x, model.InvitedCount); + return hasChanged; + } + + /// + /// Fetches the users data from the REST API to update this object, + /// especially the property. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous reloading operation. + /// + public Task UpdateAsync(RequestOptions? options = null) => + SocketUserHelper.UpdateAsync(this, Kook, options); + + private string DebuggerDisplay => + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")}, Self)"; + internal new SocketSelfUser Clone() => (SocketSelfUser)MemberwiseClone(); #region ISelfUser /// - public async Task StartPlayingAsync(IGame game, RequestOptions options = null) - => await UserHelper.StartPlayingAsync(this, Kook, game, options).ConfigureAwait(false); + public async Task StartPlayingAsync(IGame game, RequestOptions? options = null) => + await UserHelper.StartPlayingAsync(this, Kook, game, options).ConfigureAwait(false); /// - public async Task StartPlayingAsync(Music music, RequestOptions options = null) - => await UserHelper.StartPlayingAsync(this, Kook, music, options).ConfigureAwait(false); + public async Task StartPlayingAsync(Music music, RequestOptions? options = null) => + await UserHelper.StartPlayingAsync(this, Kook, music, options).ConfigureAwait(false); /// - public async Task StopPlayingAsync(ActivityType type, RequestOptions options = null) - => await UserHelper.StopPlayingAsync(this, Kook, type, options).ConfigureAwait(false); + public async Task StopPlayingAsync(ActivityType type, RequestOptions? options = null) => + await UserHelper.StopPlayingAsync(this, Kook, type, options).ConfigureAwait(false); #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index e0bb4738..5833498b 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -8,14 +8,14 @@ namespace Kook.WebSocket; /// /// A user may not be recognized due to the user missing from the cache or failed to be recognized properly. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketUnknownUser : SocketUser { /// - public override string Username { get; internal set; } + public override string Username { get; internal set; } = string.Empty; /// - public override ushort? IdentifyNumberValue { get; internal set; } + public override ushort IdentifyNumberValue { get; internal set; } /// public override bool? IsBot { get; internal set; } @@ -30,30 +30,27 @@ public class SocketUnknownUser : SocketUser public override bool? HasAnnualBuff { get; internal set; } /// - public override string Avatar { get; internal set; } + public override string Avatar { get; internal set; } = string.Empty; /// - public override string BuffAvatar { get; internal set; } + public override string BuffAvatar { get; internal set; } = string.Empty; /// - public override string Banner { get; internal set; } + public override string? Banner { get; internal set; } = string.Empty; /// public override bool? IsDenoiseEnabled { get; internal set; } /// - public override UserTag UserTag { get; internal set; } + public override UserTag? UserTag { get; internal set; } /// - public override IReadOnlyCollection Nameplates { get; internal set; } - - /// - public override bool? IsSystemUser { get; internal set; } + public override IReadOnlyCollection Nameplates { get; internal set; } = []; /// internal override SocketPresence Presence { - get => new(null, null); + get => new(); set { } } @@ -72,6 +69,8 @@ internal static SocketUnknownUser Create(KookSocketClient kook, ClientState stat return entity; } - private string DebuggerDisplay => $"{Username}#{IdentifyNumber} ({Id}{(IsBot ?? false ? ", Bot" : "")}, Unknown)"; - internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; + private string DebuggerDisplay => + $"{this.UsernameAndIdentifyNumber(Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")}, Unknown)"; + internal new SocketUnknownUser Clone() => (SocketUnknownUser)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketUser.cs index 00d705ec..fb1d15ea 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketUser.cs @@ -9,14 +9,14 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket-based user. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class SocketUser : SocketEntity, IUser { /// public abstract string Username { get; internal set; } /// - public abstract ushort? IdentifyNumberValue { get; internal set; } + public abstract ushort IdentifyNumberValue { get; internal set; } /// public abstract bool? IsBot { get; internal set; } @@ -37,25 +37,26 @@ public abstract class SocketUser : SocketEntity, IUser public abstract string BuffAvatar { get; internal set; } /// - public abstract string Banner { get; internal set; } + public abstract string? Banner { get; internal set; } /// public abstract bool? IsDenoiseEnabled { get; internal set; } /// - public abstract UserTag UserTag { get; internal set; } + public abstract UserTag? UserTag { get; internal set; } /// public abstract IReadOnlyCollection Nameplates { get; internal set; } /// - public abstract bool? IsSystemUser { get; internal set; } + public bool IsSystemUser { get; internal set; } internal abstract SocketGlobalUser GlobalUser { get; } + internal abstract SocketPresence Presence { get; set; } /// - public string IdentifyNumber => IdentifyNumberValue?.ToString("D4"); + public string IdentifyNumber => IdentifyNumberValue.ToString("D4"); /// public string KMarkdownMention => MentionUtils.KMarkdownMentionUser(Id); @@ -77,157 +78,82 @@ public abstract class SocketUser : SocketEntity, IUser protected SocketUser(KookSocketClient kook, ulong id) : base(kook, id) { + IsSystemUser = Id == KookConfig.SystemMessageAuthorID; } internal virtual bool Update(ClientState state, API.Gateway.UserUpdateEvent model) { bool hasChanges = false; - - if (model.Username != Username) - { - Username = model.Username; - hasChanges = true; - } - - if (model.Avatar != Avatar) + hasChanges |= ValueHelper.SetIfChanged(() => Username, x => Username = x, model.Username); + if (hasChanges) { - Avatar = model.Avatar; - hasChanges = true; + foreach (SocketGuildUser current in state.Guilds.Select(x => x.CurrentUser).OfType()) + current.UpdateNickname(); } + hasChanges |= ValueHelper.SetIfChanged(() => Avatar, x => Avatar = x, model.Avatar); return hasChanges; } internal virtual bool Update(ClientState state, Model model) { bool hasChanges = false; - - if (model.Username != Username) - { - Username = model.Username; - hasChanges = true; - } - - if (model.IdentifyNumber != IdentifyNumber) - { - ushort newVal = ushort.Parse(model.IdentifyNumber, NumberStyles.None, CultureInfo.InvariantCulture); - if (newVal != IdentifyNumberValue) - { - IdentifyNumberValue = newVal; - hasChanges = true; - } - } - - if (model.Bot != IsBot) - { - IsBot = model.Bot; - hasChanges = true; - } - - if (model.Status == 10 != IsBanned) - { - IsBanned = model.Status == 10; - hasChanges = true; - } - - if (model.HasBuff != HasBuff) - { - HasBuff = model.HasBuff; - hasChanges = true; - } - - if (model.HasAnnualBuff != HasAnnualBuff) - { - HasAnnualBuff = model.HasAnnualBuff; - hasChanges = true; - } - - if (model.Avatar != Avatar) - { - Avatar = model.Avatar; - hasChanges = true; - } - - if (model.BuffAvatar != BuffAvatar) - { - BuffAvatar = model.BuffAvatar; - hasChanges = true; - } - - if (model.Banner != Banner) - { - Banner = model.Banner; - hasChanges = true; - } - - if (model.IsDenoiseEnabled != IsDenoiseEnabled) - { - IsDenoiseEnabled = model.IsDenoiseEnabled; - hasChanges = true; - } - - UserTag userTag = model.UserTag.ToEntity(); - if (model.UserTag is not null && !userTag.Equals(UserTag)) - { - UserTag = userTag; - hasChanges = true; - } - - IReadOnlyCollection nameplates = model.Nameplates?.Select(x => x.ToEntity()) - .ToImmutableList() ?? ImmutableList.Empty; - if (Nameplates is null || !nameplates.SequenceEqual(Nameplates)) - { - Nameplates = nameplates; - hasChanges = true; - } - - if (model.IsSystemUser != IsSystemUser) - { - IsSystemUser = model.IsSystemUser; - hasChanges = true; - } - + hasChanges |= ValueHelper.SetIfChanged(() => Username, x => Username = x, model.Username); + hasChanges |= ValueHelper.SetIfChanged(() => IdentifyNumberValue, x => IdentifyNumberValue = x, + ushort.Parse(model.IdentifyNumber, NumberStyles.None, CultureInfo.InvariantCulture)); + hasChanges |= ValueHelper.SetIfChanged(() => IsBot, x => IsBot = x, model.Bot); + hasChanges |= ValueHelper.SetIfChanged(() => IsBanned, x => IsBanned = x, model.Status == 10); + hasChanges |= ValueHelper.SetIfChanged(() => HasBuff, x => HasBuff = x, model.HasBuff); + hasChanges |= ValueHelper.SetIfChanged(() => HasAnnualBuff, x => HasAnnualBuff = x, model.HasAnnualBuff); + hasChanges |= ValueHelper.SetIfChanged(() => Avatar, x => Avatar = x, model.Avatar); + hasChanges |= ValueHelper.SetIfChanged(() => BuffAvatar, x => BuffAvatar = x, model.BuffAvatar); + hasChanges |= ValueHelper.SetIfChanged(() => Banner, x => Banner = x, model.Banner); + hasChanges |= ValueHelper.SetIfChanged(() => IsDenoiseEnabled, x => IsDenoiseEnabled = x, model.IsDenoiseEnabled); + hasChanges |= ValueHelper.SetIfChanged(() => UserTag, x => UserTag = x, model.UserTag?.ToEntity()); + hasChanges |= ValueHelper.SetIfChanged(() => Nameplates, x => Nameplates = x, + model.Nameplates?.Select(x => x.ToEntity()).ToImmutableList() ?? [], + (x, y) => x.SequenceEqual(y)); + if (model.IsSystemUser.HasValue) + hasChanges |= ValueHelper.SetIfChanged(() => IsSystemUser, x => IsSystemUser = x, model.IsSystemUser.Value); return hasChanges; } internal virtual void UpdatePresence(bool? isOnline) { - Presence ??= new SocketPresence(); Presence.Update(isOnline); } - internal virtual void UpdatePresence(bool? isOnline, string activeClient) + internal virtual void UpdatePresence(bool? isOnline, string? activeClient) { - Presence ??= new SocketPresence(); Presence.Update(isOnline, activeClient); } /// - public async Task CreateDMChannelAsync(RequestOptions options = null) - => await SocketUserHelper.CreateDMChannelAsync(this, Kook, options).ConfigureAwait(false); + public async Task CreateDMChannelAsync(RequestOptions? options = null) => + await SocketUserHelper.CreateDMChannelAsync(this, Kook, options).ConfigureAwait(false); /// - public Task GetIntimacyAsync(RequestOptions options = null) - => UserHelper.GetIntimacyAsync(this, Kook, options); + public Task GetIntimacyAsync(RequestOptions? options = null) => + UserHelper.GetIntimacyAsync(this, Kook, options); /// - public async Task UpdateIntimacyAsync(Action func, RequestOptions options = null) - => await UserHelper.UpdateIntimacyAsync(this, Kook, func, options).ConfigureAwait(false); + public async Task UpdateIntimacyAsync(Action func, RequestOptions? options = null) => + await UserHelper.UpdateIntimacyAsync(this, Kook, func, options).ConfigureAwait(false); /// - public Task BlockAsync(RequestOptions options = null) => + public Task BlockAsync(RequestOptions? options = null) => UserHelper.BlockAsync(this, Kook, options); /// - public Task UnblockAsync(RequestOptions options = null) => + public Task UnblockAsync(RequestOptions? options = null) => UserHelper.UnblockAsync(this, Kook, options); /// - public Task RequestFriendAsync(RequestOptions options = null) => + public Task RequestFriendAsync(RequestOptions? options = null) => UserHelper.RequestFriendAsync(this, Kook, options); /// - public Task RemoveFriendAsync(RequestOptions options = null) => + public Task RemoveFriendAsync(RequestOptions? options = null) => UserHelper.RemoveFriendAsync(this, Kook, options); /// @@ -239,23 +165,24 @@ public Task RemoveFriendAsync(RequestOptions options = null) => public override string ToString() => Format.UsernameAndIdentifyNumber(this, Kook.FormatUsersInBidirectionalUnicode); private string DebuggerDisplay => - $"{Format.UsernameAndIdentifyNumber(this, Kook.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ?? false ? ", Bot" : "")})"; + $"{Format.UsernameAndIdentifyNumber(this, Kook.FormatUsersInBidirectionalUnicode)} ({Id}{ + (IsBot ?? false ? ", Bot" : "")})"; - internal SocketUser Clone() => MemberwiseClone() as SocketUser; + internal SocketUser Clone() => (SocketUser)MemberwiseClone(); #region IUser /// - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.CreateDMChannelAsync(RequestOptions? options) => + await CreateDMChannelAsync(options).ConfigureAwait(false); /// - async Task IUser.GetIntimacyAsync(RequestOptions options) - => await GetIntimacyAsync(options).ConfigureAwait(false); + async Task IUser.GetIntimacyAsync(RequestOptions? options) => + await GetIntimacyAsync(options).ConfigureAwait(false); /// - async Task IUser.UpdateIntimacyAsync(Action func, RequestOptions options) - => await UpdateIntimacyAsync(func, options).ConfigureAwait(false); + async Task IUser.UpdateIntimacyAsync(Action func, RequestOptions? options) => + await UpdateIntimacyAsync(func, options).ConfigureAwait(false); #endregion } diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketUserHelper.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketUserHelper.cs index 29f51d66..0c8b7a9f 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketUserHelper.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketUserHelper.cs @@ -7,20 +7,26 @@ namespace Kook.WebSocket; internal static class SocketUserHelper { public static async Task> GetConnectedChannelsAsync(IGuildUser user, BaseSocketClient client, - RequestOptions options) + RequestOptions? options) { IEnumerable channels = await client.ApiClient.GetAudioChannelsUserConnectsAsync(user.GuildId, user.Id, options: options) .FlattenAsync().ConfigureAwait(false); return channels.Select(x => client.GetChannel(x.Id) as SocketVoiceChannel).ToImmutableArray(); } - public static async Task UpdateAsync(SocketGuildUser user, KookSocketClient client, RequestOptions options) + public static async Task UpdateAsync(SocketGuildUser user, KookSocketClient client, RequestOptions? options) { GuildMember member = await client.ApiClient.GetGuildMemberAsync(user.Guild.Id, user.Id, options).ConfigureAwait(false); user.Update(client.State, member); } - public static async Task CreateDMChannelAsync(SocketUser user, KookSocketClient client, RequestOptions options) + public static async Task UpdateAsync(SocketSelfUser user, KookSocketClient client, RequestOptions? options) + { + SelfUser selfUser = await client.ApiClient.GetSelfUserAsync(options).ConfigureAwait(false); + user.Update(client.State, selfUser); + } + + public static async Task CreateDMChannelAsync(SocketUser user, KookSocketClient client, RequestOptions? options) { UserChat userChat = await client.ApiClient.CreateUserChatAsync(user.Id, options).ConfigureAwait(false); return SocketDMChannel.Create(client, client.State, userChat.Code, userChat.Recipient); diff --git a/src/Kook.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Kook.Net.WebSocket/Entities/Users/SocketVoiceState.cs index b1cfc3eb..4de8710b 100644 --- a/src/Kook.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Kook.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -5,7 +5,7 @@ namespace Kook.WebSocket; /// /// Represents a WebSocket user's voice connection status. /// -[DebuggerDisplay(@"{DebuggerDisplay,nq}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] public struct SocketVoiceState : IVoiceState { /// @@ -19,7 +19,7 @@ public struct SocketVoiceState : IVoiceState /// The voice channel that the user is currently in. /// Whether the user is muted. /// Whether the user is deafened. - public SocketVoiceState(SocketVoiceChannel voiceChannel, bool? isMuted, bool? isDeafened) + public SocketVoiceState(SocketVoiceChannel? voiceChannel, bool? isMuted, bool? isDeafened) { VoiceChannel = voiceChannel; IsMuted = isMuted; @@ -29,7 +29,7 @@ public SocketVoiceState(SocketVoiceChannel voiceChannel, bool? isMuted, bool? is /// /// Gets the voice channel that the user is currently in; or null if none. /// - public SocketVoiceChannel VoiceChannel { get; private set; } + public SocketVoiceChannel? VoiceChannel { get; private set; } /// public bool? IsMuted { get; private set; } @@ -37,13 +37,14 @@ public SocketVoiceState(SocketVoiceChannel voiceChannel, bool? isMuted, bool? is /// public bool? IsDeafened { get; private set; } - internal void Update(SocketVoiceChannel voiceChannel) => VoiceChannel = voiceChannel; + internal void Update(SocketVoiceChannel? voiceChannel) => VoiceChannel = voiceChannel; internal void Update(bool? isMuted, bool? isDeafened) { - if (isMuted.HasValue) IsMuted = isMuted.Value; - - if (isDeafened.HasValue) IsDeafened = isDeafened.Value; + if (isMuted.HasValue) + IsMuted = isMuted.Value; + if (isDeafened.HasValue) + IsDeafened = isDeafened.Value; } /// @@ -55,10 +56,20 @@ internal void Update(bool? isMuted, bool? isDeafened) public override string ToString() => VoiceChannel?.Name ?? "Unknown"; private string DebuggerDisplay => - $"{VoiceChannel?.Name ?? "Unknown"} ({(IsMuted.HasValue ? IsMuted.Value ? "Muted" : "Unmuted" : "Unknown")} / {(IsDeafened.HasValue ? IsDeafened.Value ? "Deafened" : "Undeafened" : "Unknown")})"; - - internal SocketVoiceState Clone() => this; + $"{VoiceChannel?.Name ?? "Unknown"} ({ + IsMuted switch + { + true => "Muted", + false => "Unmuted", + _ => "Unknown" + }} / { + IsDeafened switch + { + true => "Deafened", + false => "Undeafened", + _ => "Unknown" + }})"; /// - IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + IVoiceChannel? IVoiceState.VoiceChannel => VoiceChannel; } diff --git a/src/Kook.Net.WebSocket/Extensions/SocketEntityExtensions.cs b/src/Kook.Net.WebSocket/Extensions/SocketEntityExtensions.cs index a7ab1c00..410ad907 100644 --- a/src/Kook.Net.WebSocket/Extensions/SocketEntityExtensions.cs +++ b/src/Kook.Net.WebSocket/Extensions/SocketEntityExtensions.cs @@ -2,8 +2,8 @@ namespace Kook.WebSocket; internal static class SocketEntityExtensions { - public static GuildEmote ToEntity(this API.Gateway.GuildEmojiEvent model, ulong guildId) - => new(model.Id, + public static GuildEmote ToEntity(this API.Gateway.GuildEmojiEvent model, ulong guildId) => + new(model.Id, model.Name, model.Type == EmojiType.Animated, guildId, diff --git a/src/Kook.Net.WebSocket/Kook.Net.WebSocket.csproj b/src/Kook.Net.WebSocket/Kook.Net.WebSocket.csproj index e50056f9..2dcc03a2 100644 --- a/src/Kook.Net.WebSocket/Kook.Net.WebSocket.csproj +++ b/src/Kook.Net.WebSocket/Kook.Net.WebSocket.csproj @@ -7,15 +7,11 @@ true Kook.Net.WebSocket The WebSocket API implementation for Kook.Net. - - - true - TRACE_PACKETS - - true + + DEBUG_PACKETS diff --git a/src/Kook.Net.WebSocket/KookSocketApiClient.cs b/src/Kook.Net.WebSocket/KookSocketApiClient.cs index 3ee9eb62..f5cff973 100644 --- a/src/Kook.Net.WebSocket/KookSocketApiClient.cs +++ b/src/Kook.Net.WebSocket/KookSocketApiClient.cs @@ -16,6 +16,7 @@ namespace Kook.API; internal class KookSocketApiClient : KookRestApiClient { + private static readonly JsonElement EmptyJsonElement = JsonDocument.Parse("{}").RootElement; public event Func SentGatewayMessage { add => _sentGatewayMessageEvent.Add(value); @@ -24,13 +25,13 @@ public event Func SentGatewayMessage private readonly AsyncEvent> _sentGatewayMessageEvent = new(); - public event Func ReceivedGatewayEvent + public event Func ReceivedGatewayEvent { add => _receivedGatewayEvent.Add(value); remove => _receivedGatewayEvent.Remove(value); } - private readonly AsyncEvent> _receivedGatewayEvent = new(); + private readonly AsyncEvent> _receivedGatewayEvent = new(); public event Func Disconnected { @@ -41,21 +42,33 @@ public event Func Disconnected private readonly AsyncEvent> _disconnectedEvent = new(); private readonly bool _isExplicitUrl; - private CancellationTokenSource _connectCancellationToken; - private string _gatewayUrl; + private CancellationTokenSource? _connectCancellationToken; + private string? _gatewayUrl; private Guid? _sessionId; private int _lastSeq; + +#if DEBUG_PACKETS + private readonly JsonSerializerOptions _debugJsonSerializerOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; +#endif + public ConnectionState ConnectionState { get; private set; } internal IWebSocketClient WebSocketClient { get; } - public KookSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, string acceptLanguage, - string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializerOptions serializerOptions = null, - Func defaultRatelimitCallback = null) - : base(restClientProvider, userAgent, acceptLanguage, defaultRetryMode, serializerOptions, defaultRatelimitCallback) + public KookSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, + string userAgent, string acceptLanguage, string? url = null, + RetryMode defaultRetryMode = RetryMode.AlwaysRetry, + JsonSerializerOptions? serializerOptions = null, + Func? defaultRatelimitCallback = null) + : base(restClientProvider, userAgent, acceptLanguage, + defaultRetryMode, serializerOptions, defaultRatelimitCallback) { _gatewayUrl = url; - if (url != null) _isExplicitUrl = true; - + if (url != null) + _isExplicitUrl = true; WebSocketClient = webSocketProvider(); WebSocketClient.SetKeepAliveInterval(TimeSpan.Zero); WebSocketClient.TextMessage += OnTextMessage; @@ -65,7 +78,6 @@ public KookSocketApiClient(RestClientProvider restClientProvider, WebSocketProvi #if DEBUG_PACKETS Debug.WriteLine(ex); #endif - await DisconnectAsync().ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); }; @@ -89,47 +101,47 @@ private async Task OnBinaryMessage(byte[] data, int index, int count) await decompressor.CopyToAsync(decompressed); decompressed.Position = 0; - GatewaySocketFrame gatewaySocketFrame = JsonSerializer.Deserialize(decompressed, _serializerOptions); + GatewaySocketFrame? gatewaySocketFrame = JsonSerializer + .Deserialize(decompressed, _serializerOptions); if (gatewaySocketFrame is not null) { #if DEBUG_PACKETS string raw = Encoding.Default.GetString(decompressed.ToArray()).TrimEnd('\n'); - string parsed = JsonSerializer.Serialize(gatewaySocketFrame.Payload, new JsonSerializerOptions - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }).TrimEnd('\n'); + string parsed = JsonSerializer + .Serialize(gatewaySocketFrame.Payload, _debugJsonSerializerOptions) + .TrimEnd('\n'); Debug.WriteLine($""" <- [{gatewaySocketFrame.Type}] : #{gatewaySocketFrame.Sequence} [Raw] {raw} [Parsed] {parsed} """); #endif - await _receivedGatewayEvent.InvokeAsync(gatewaySocketFrame.Type, gatewaySocketFrame.Sequence, gatewaySocketFrame.Payload) + JsonElement payloadElement = gatewaySocketFrame.Payload as JsonElement? ?? EmptyJsonElement; + await _receivedGatewayEvent + .InvokeAsync(gatewaySocketFrame.Type, gatewaySocketFrame.Sequence, payloadElement) .ConfigureAwait(false); } } private async Task OnTextMessage(string message) { - GatewaySocketFrame gatewaySocketFrame = JsonSerializer.Deserialize(message, _serializerOptions); - if (gatewaySocketFrame is not null) - { + GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(message, _serializerOptions); + if (gatewaySocketFrame is null) + return; #if DEBUG_PACKETS - string parsed = JsonSerializer.Serialize(gatewaySocketFrame.Payload, new JsonSerializerOptions - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }).TrimEnd('\n'); - Debug.WriteLine($""" - <- [{gatewaySocketFrame.Type}] : #{gatewaySocketFrame.Sequence} - [Raw] {message} - [Parsed] {parsed} - """); + string parsed = JsonSerializer + .Serialize(gatewaySocketFrame.Payload, _debugJsonSerializerOptions) + .TrimEnd('\n'); + Debug.WriteLine($""" + <- [{gatewaySocketFrame.Type}] : #{gatewaySocketFrame.Sequence} + [Raw] {message} + [Parsed] {parsed} + """); #endif - await _receivedGatewayEvent.InvokeAsync(gatewaySocketFrame.Type, gatewaySocketFrame.Sequence, gatewaySocketFrame.Payload) - .ConfigureAwait(false); - } + JsonElement payloadElement = gatewaySocketFrame.Payload as JsonElement? ?? EmptyJsonElement; + await _receivedGatewayEvent + .InvokeAsync(gatewaySocketFrame.Type, gatewaySocketFrame.Sequence, payloadElement) + .ConfigureAwait(false); } internal override void Dispose(bool disposing) @@ -165,31 +177,30 @@ public async Task ConnectAsync(Guid? sessionId, int lastSeq) internal override async Task ConnectInternalAsync() { - if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("The client must be logged in before connecting."); - - if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with WebSocket support."); - + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("The client must be logged in before connecting."); + if (WebSocketClient == null) + throw new NotSupportedException("This client is not configured with WebSocket support."); RequestQueue.ClearGatewayBuckets(); - ConnectionState = ConnectionState.Connecting; - try { _connectCancellationToken?.Dispose(); _connectCancellationToken = new CancellationTokenSource(); - WebSocketClient?.SetCancellationToken(_connectCancellationToken.Token); + WebSocketClient.SetCancellationToken(_connectCancellationToken.Token); - if (!_isExplicitUrl) + if (!_isExplicitUrl || _gatewayUrl == null) { GetBotGatewayResponse botGatewayResponse = await GetBotGatewayAsync().ConfigureAwait(false); - _gatewayUrl = $"{botGatewayResponse.Url}{(_sessionId is null ? string.Empty : $"&resume=1&sn={_lastSeq}&session_id={_sessionId}")}"; + string resumeQuery = _sessionId is not null + ? $"&resume=1&sn={_lastSeq}&session_id={_sessionId}" + : string.Empty; + _gatewayUrl = $"{botGatewayResponse.Url}{resumeQuery}"; } - #if DEBUG_PACKETS Debug.WriteLine("Connecting to gateway: " + _gatewayUrl); #endif - - await WebSocketClient!.ConnectAsync(_gatewayUrl).ConfigureAwait(false); + await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } catch (Exception) @@ -201,7 +212,7 @@ internal override async Task ConnectInternalAsync() } } - public async Task DisconnectAsync(Exception ex = null) + public async Task DisconnectAsync(Exception? ex = null) { await _stateLock.WaitAsync().ConfigureAwait(false); try @@ -214,12 +225,12 @@ public async Task DisconnectAsync(Exception ex = null) } } - internal override async Task DisconnectInternalAsync(Exception ex = null) + internal override async Task DisconnectInternalAsync(Exception? ex = null) { - if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with WebSocket support."); - - if (ConnectionState == ConnectionState.Disconnected) return; - + if (WebSocketClient == null) + throw new NotSupportedException("This client is not configured with WebSocket support."); + if (ConnectionState == ConnectionState.Disconnected) + return; ConnectionState = ConnectionState.Disconnecting; if (ex is GatewayReconnectException) @@ -239,40 +250,47 @@ internal override async Task DisconnectInternalAsync(Exception ex = null) ConnectionState = ConnectionState.Disconnected; } - public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) + public async Task SendHeartbeatAsync(int lastSeq, RequestOptions? options = null) { - options = RequestOptions.CreateOrClone(options); - await SendGatewayAsync(GatewaySocketFrameType.Ping, sequence: lastSeq, options: options).ConfigureAwait(false); + RequestOptions requestOptions = RequestOptions.CreateOrClone(options); + await SendGatewayAsync(GatewaySocketFrameType.Ping, null, lastSeq, requestOptions) + .ConfigureAwait(false); } - public async Task SendResumeAsync(int lastSeq, RequestOptions options = null) + public async Task SendResumeAsync(int lastSeq, RequestOptions? options = null) { - options = RequestOptions.CreateOrClone(options); - await SendGatewayAsync(GatewaySocketFrameType.Resume, sequence: lastSeq, options: options).ConfigureAwait(false); + RequestOptions requestOptions = RequestOptions.CreateOrClone(options); + await SendGatewayAsync(GatewaySocketFrameType.Resume, null, lastSeq, requestOptions) + .ConfigureAwait(false); } - public Task SendGatewayAsync(GatewaySocketFrameType gatewaySocketFrameType, object payload = null, int? sequence = null, - RequestOptions options = null) - => SendGatewayInternalAsync(gatewaySocketFrameType, options, payload, sequence); + public Task SendGatewayAsync(GatewaySocketFrameType gatewaySocketFrameType, + object? payload, int? sequence, RequestOptions options) => + SendGatewayInternalAsync(gatewaySocketFrameType, payload, sequence, options); - private async Task SendGatewayInternalAsync(GatewaySocketFrameType gatewaySocketFrameType, RequestOptions options, object payload = null, - int? sequence = null) + private async Task SendGatewayInternalAsync(GatewaySocketFrameType gatewaySocketFrameType, + object? payload, int? sequence, RequestOptions options) { CheckState(); - - payload = new GatewaySocketFrame { Type = gatewaySocketFrameType, Payload = payload, Sequence = sequence }; - byte[] bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + payload = new GatewaySocketFrame + { + Type = gatewaySocketFrameType, + Payload = payload, + Sequence = sequence + }; + string json = SerializeJson(payload); + byte[] bytes = Encoding.UTF8.GetBytes(json); options.IsGatewayBucket = true; - if (options.BucketId == null) options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id; - + options.BucketId ??= GatewayBucket.Get(GatewayBucketType.Unbucketed).Id; + bool ignoreLimit = gatewaySocketFrameType == GatewaySocketFrameType.Ping; await RequestQueue - .SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, gatewaySocketFrameType == GatewaySocketFrameType.Ping, options)) + .SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, ignoreLimit, options)) .ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(gatewaySocketFrameType).ConfigureAwait(false); #if DEBUG_PACKETS - string payloadString = JsonSerializer.Serialize(payload, new JsonSerializerOptions {WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}); + string payloadString = JsonSerializer.Serialize(payload, _debugJsonSerializerOptions); Debug.WriteLine($"-> [{gatewaySocketFrameType}] : #{sequence} \n{payloadString}".TrimEnd('\n')); #endif } diff --git a/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs b/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs new file mode 100644 index 00000000..96b04257 --- /dev/null +++ b/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs @@ -0,0 +1,1251 @@ +using System.Collections.Immutable; +using System.Net; +using System.Text.Json; +using Kook.API; +using Kook.API.Gateway; +using Kook.API.Rest; +using Kook.Net; +using Kook.Rest; +using Reaction = Kook.API.Gateway.Reaction; + +namespace Kook.WebSocket; + +public partial class KookSocketClient +{ + #region Gateway + + private async Task HandleGatewayHelloAsync(JsonElement payload) + { + if (DeserializePayload(payload) is not { } gatewayHelloPayload) return; + await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); + try + { + _sessionId = gatewayHelloPayload.SessionId; + _heartbeatTask = RunHeartbeatAsync(_connection.CancellationToken); + } + catch (Exception ex) + { + _connection.CriticalError(new Exception("Processing Hello failed", ex)); + return; + } + + // Get current user + try + { + SelfUser selfUser = await ApiClient.GetSelfUserAsync().ConfigureAwait(false); + SocketSelfUser currentUser = SocketSelfUser.Create(this, State, selfUser); + Rest.CreateRestSelfUser(selfUser); + ApiClient.CurrentUserId = currentUser.Id; + Rest.CurrentUser = RestSelfUser.Create(this, selfUser); + CurrentUser = currentUser; + } + catch (Exception ex) + { + _connection.CriticalError(new Exception("Processing SelfUser failed", ex)); + return; + } + + // Download guild data + try + { + IReadOnlyCollection guilds = await ApiClient.ListGuildsAsync().ConfigureAwait(false); + ClientState state = new(guilds.Count, 0); + _unavailableGuildCount = 0; + foreach (RichGuild guild in guilds) + { + SocketGuild socketGuild = AddGuild(guild, state); + if (!socketGuild.IsAvailable) + _unavailableGuildCount++; + else + await GuildAvailableAsync(socketGuild).ConfigureAwait(false); + } + + State = state; + } + catch (Exception ex) + { + _connection.CriticalError(new Exception("Processing Guilds failed", ex)); + return; + } + + _lastGuildAvailableTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _guildDownloadTask = WaitForGuildsAsync(_connection.CancellationToken, _gatewayLogger) + .ContinueWith(async task => + { + if (task.IsFaulted) + { + Exception exception = task.Exception + ?? new Exception("Waiting for guilds failed without an exception"); + _connection.Error(exception); + return; + } + + if (_connection.CancellationToken.IsCancellationRequested) return; + + // Download user list if enabled + if (BaseConfig.AlwaysDownloadUsers) + { + _ = Task.Run(async () => + { + try + { + await DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && x.HasAllMembers is not true)); + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync("Downloading users failed", ex).ConfigureAwait(false); + } + }); + } + + if (BaseConfig.AlwaysDownloadVoiceStates) + { + _ = Task.Run(async () => + { + try + { + await DownloadVoiceStatesAsync(Guilds.Where(x => x.IsAvailable)); + } + catch (Exception ex) + { + await _gatewayLogger + .WarningAsync("Downloading voice states failed", ex) + .ConfigureAwait(false); + } + }); + } + + if (BaseConfig.AlwaysDownloadBoostSubscriptions) + { + _ = Task.Run(async () => + { + try + { + await DownloadBoostSubscriptionsAsync(Guilds.Where(x => x.IsAvailable)); + } + catch (Exception ex) + { + await _gatewayLogger + .WarningAsync("Downloading boost subscriptions failed", ex) + .ConfigureAwait(false); + } + }); + } + + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); + }); + + _ = _connection.CompleteAsync(); + } + + private async Task HandlePongAsync() + { + await _gatewayLogger.DebugAsync("Received Pong").ConfigureAwait(false); + if (_heartbeatTimes.TryDequeue(out long time)) + { + int latency = (int)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - time); + int before = Latency; + Latency = latency; + + await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency) + .ConfigureAwait(false); + } + } + + private async Task HandleReconnectAsync(JsonElement payload) + { + GatewayReconnectPayload? reconnectPayload = DeserializePayload(payload); + await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); + if (reconnectPayload?.Code is KookErrorCode.MissingResumeArgument + or KookErrorCode.SessionExpired + or KookErrorCode.InvalidSequenceNumber) + { + _sessionId = null; + _lastSeq = 0; + } + + string reason = reconnectPayload?.Message is not null && !string.IsNullOrWhiteSpace(reconnectPayload.Message) + ? $": {reconnectPayload.Message}" + : "."; + GatewayReconnectException exception = new($"Server requested a reconnect, resuming session failed{reason}"); + _connection.Error(exception); + } + + private async Task HandleResumeAckAsync() + { + await _gatewayLogger.DebugAsync("Received ResumeAck").ConfigureAwait(false); + _ = _connection.CompleteAsync(); + + //Notify the client that these guilds are available again + foreach (SocketGuild guild in State.Guilds) + { + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } + + #endregion + + #region Messages + + private async Task HandleGroupMessage(GatewayEvent gatewayEvent) + { + if (GetGuild(gatewayEvent.ExtraData.GuildId) is not { } guild) + { + await UnknownGuildAsync($"{gatewayEvent.Type.ToString()}: {gatewayEvent.ExtraData.Type}", + gatewayEvent.ExtraData.GuildId, gatewayEvent) + .ConfigureAwait(false); + return; + } + if (guild.GetTextChannel(gatewayEvent.TargetId) is not { } channel) + { + await UnknownChannelAsync($"{gatewayEvent.Type.ToString()}: {gatewayEvent.ExtraData.Type}", + gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + SocketGuildUser author = guild.GetUser(gatewayEvent.ExtraData.Author.Id) + ?? guild.AddOrUpdateUser(gatewayEvent.ExtraData.Author); + SocketMessage msg = SocketMessage.Create(this, State, author, channel, gatewayEvent); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), + msg, author, channel).ConfigureAwait(false); + } + + private async Task HandlePersonMessage(GatewayEvent gatewayEvent) + { + SocketUser author = State.GetOrAddUser(gatewayEvent.ExtraData.Author.Id, + _ => SocketGlobalUser.Create(this, State, gatewayEvent.ExtraData.Author)); + SocketDMChannel channel = GetDMChannel(gatewayEvent.ExtraData.Code) + ?? AddDMChannel(gatewayEvent.ExtraData.Code, gatewayEvent.ExtraData.Author, State); + if (author is null) + { + await UnknownChannelUserAsync($"{gatewayEvent.Type.ToString()}: {gatewayEvent.ExtraData.Type}", + gatewayEvent.ExtraData.Author.Id, gatewayEvent.ExtraData.Code, gatewayEvent) + .ConfigureAwait(false); + return; + } + SocketMessage msg = SocketMessage.Create(this, State, author, channel, gatewayEvent); + SocketChannelHelper.AddMessage(channel, this, msg); + + await TimedInvokeAsync(_directMessageReceivedEvent, nameof(DirectMessageReceived), + msg, author, channel).ConfigureAwait(false); + } + + #endregion + + #region Channels + + /// + /// "GROUP", "added_reaction" + /// + private async Task HandleAddedReaction(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent.ExtraData.Body) + .ConfigureAwait(false); + return; + } + + SocketUserMessage? cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketUser? user = GetUser(data.UserId); + SocketGuildUser? socketGuildUser = channel.GetUser(data.UserId); + SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, socketGuildUser ?? user); + Cacheable cacheableUser = + new(socketGuildUser, data.UserId, socketGuildUser is not null, + async () => + { + GuildMember model = await ApiClient + .GetGuildMemberAsync(channel.Guild.Id, data.UserId) + .ConfigureAwait(false); + SocketGuildUser updatedUser = channel.Guild.AddOrUpdateUser(model); + reaction.User = updatedUser; + return updatedUser; + }); + Cacheable cacheableMsg = + new(cachedMsg, data.MessageId, cachedMsg is not null, + async () => + { + IMessage message = await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false); + reaction.Message = message; + return message; + }); + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), + cacheableMsg, channel, cacheableUser, reaction).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_reaction" + /// + private async Task HandleDeletedReaction(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketUserMessage? cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketUser? user = GetUser(data.UserId); + SocketGuildUser? socketGuildUser = channel.GetUser(data.UserId); + SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, socketGuildUser ?? user); + Cacheable cacheableUser = + new(socketGuildUser, data.UserId, socketGuildUser is not null, + async () => + { + GuildMember model = await ApiClient + .GetGuildMemberAsync(channel.Guild.Id, data.UserId) + .ConfigureAwait(false); + SocketGuildUser updatedUser = channel.Guild.AddOrUpdateUser(model); + reaction.User = updatedUser; + return updatedUser; + }); + Cacheable cacheableMsg = + new(cachedMsg, data.MessageId, cachedMsg is not null, + async () => + { + IMessage message = await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false); + reaction.Message = message; + return message; + }); + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), + cacheableMsg, channel, cacheableUser, reaction).ConfigureAwait(false); + } + + /// + /// "GROUP", "updated_message" + /// + private async Task HandleUpdatedMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketMessage? cachedMsg = channel.GetCachedMessage(data.MessageId); + SocketMessage? before = cachedMsg?.Clone(); + cachedMsg?.Update(State, data); + Cacheable cacheableBefore = new(before, data.MessageId, cachedMsg is not null, + () => Task.FromResult(null)); + Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg is not null, + async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as SocketMessage); + + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), + cacheableBefore, cacheableAfter, channel).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_message" + /// + private async Task HandleDeletedMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketMessage? msg = SocketChannelHelper.RemoveMessage(channel, this, data.MessageId); + Cacheable cacheableMsg = new(msg, data.MessageId, msg is not null, + () => Task.FromResult(null)); + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), + cacheableMsg, channel).ConfigureAwait(false); + } + + /// + /// "GROUP", "added_channel" + /// + private async Task HandleAddedChannel(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, data.GuildId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketChannel channel = guild.AddChannel(State, data); + + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); + } + + /// + /// "GROUP", "updated_channel" + /// + private async Task HandleUpdatedChannel(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.Id) is not { } channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.Id, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketChannel before = channel.Clone(); + channel.Update(State, data); + await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_channel" + /// + private async Task HandleDeletedChannel(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + State.RemoveChannel(channel.Id); + + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); + } + + /// + /// "GROUP", "pinned_message" + /// + private async Task HandlePinnedMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketGuild guild = channel.Guild; + SocketGuildUser? operatorUser = guild.GetUser(data.OperatorUserId); + Cacheable cacheableOperatorUser = + new(operatorUser, data.OperatorUserId, operatorUser is not null, + async () => + { + GuildMember model = await ApiClient + .GetGuildMemberAsync(guild.Id, data.OperatorUserId).ConfigureAwait(false); + return guild.AddOrUpdateUser(model); + }); + + SocketUserMessage? cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketMessage? before = cachedMsg?.Clone(); + if (cachedMsg is not null) + cachedMsg.IsPinned = true; + + Cacheable cacheableBefore = new(before, data.MessageId, before is not null, + () => Task.FromResult(null)); + Cacheable cacheableAfter = GetCacheableSocketMessage(cachedMsg, data.MessageId, channel); + + await TimedInvokeAsync(_messagePinnedEvent, nameof(MessagePinned), + cacheableBefore, cacheableAfter, channel, cacheableOperatorUser).ConfigureAwait(false); + } + + /// + /// "GROUP", "unpinned_message" + /// + private async Task HandleUnpinnedMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (GetChannel(data.ChannelId) is not SocketTextChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketGuild guild = channel.Guild; + SocketGuildUser? operatorUser = guild.GetUser(data.OperatorUserId); + Cacheable cacheableOperatorUser = + GetCacheableSocketGuildUser(operatorUser, data.OperatorUserId, guild); + SocketUserMessage? cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketMessage? before = cachedMsg?.Clone(); + if (cachedMsg is not null) + cachedMsg.IsPinned = false; + + Cacheable cacheableBefore = new(before, data.MessageId, before is not null, + () => Task.FromResult(null)); + Cacheable cacheableAfter = GetCacheableSocketMessage(cachedMsg, data.MessageId, channel); + + await TimedInvokeAsync(_messageUnpinnedEvent, nameof(MessageUnpinned), + cacheableBefore, cacheableAfter, channel, cacheableOperatorUser).ConfigureAwait(false); + } + + #endregion + + #region Direct Messages + + /// + /// "PERSON", "updated_private_message" + /// + private async Task HandleUpdatedPrivateMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + SocketDMChannel? channel = GetDMChannel(data.ChatCode); + Cacheable cacheableChannel = + GetCacheableDMChannel(channel, data.ChatCode, data.AuthorId); + SocketMessage? cachedMsg = channel?.GetCachedMessage(data.MessageId); + SocketMessage? before = cachedMsg?.Clone(); + cachedMsg?.Update(State, data); + Cacheable cacheableBefore = new(before, data.MessageId, before is not null, + () => Task.FromResult(null)); + Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg is not null, + async () => + { + DirectMessage msg = await ApiClient + .GetDirectMessageAsync(data.MessageId, data.ChatCode) + .ConfigureAwait(false); + User userModel = msg.Author ?? await ApiClient.GetUserAsync(data.AuthorId).ConfigureAwait(false); + SocketGlobalUser author = State.AddOrUpdateUser(data.AuthorId, + _ => SocketGlobalUser.Create(this, State, userModel), + (_, x) => + { + x.Update(State, userModel); + return x; + }); + SocketDMChannel dmChannel = CreateDMChannel(data.ChatCode, author, State); + return SocketMessage.Create(this, State, author, dmChannel, msg); + }); + SocketUser? user = State.GetUser(data.AuthorId); + Cacheable cacheableUser = GetCacheableSocketUser(user, data.AuthorId); + + await TimedInvokeAsync(_directMessageUpdatedEvent, nameof(DirectMessageUpdated), + cacheableBefore, cacheableAfter, cacheableUser, cacheableChannel).ConfigureAwait(false); + } + + /// + /// "PERSON", "deleted_private_message" + /// + private async Task HandleDeletedPrivateMessage(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + SocketDMChannel? channel = GetDMChannel(data.ChatCode); + Cacheable cacheableChannel = + GetCacheableDMChannel(channel, data.ChatCode, data.AuthorId); + SocketMessage? cachedMsg = channel?.GetCachedMessage(data.MessageId); + Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, + () => Task.FromResult(null)); + SocketUser? user = State.GetUser(data.AuthorId); + Cacheable cacheableUser = GetCacheableSocketUser(user, data.AuthorId); + + await TimedInvokeAsync(_directMessageDeletedEvent, nameof(DirectMessageDeleted), + cacheableMsg, cacheableUser, cacheableChannel).ConfigureAwait(false); + } + + /// + /// "PERSON", "private_added_reaction" + /// + private async Task HandlePrivateAddedReaction(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + SocketDMChannel? channel = GetDMChannel(data.ChatCode); + SocketUserMessage? cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketUser? operatorUser = GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketUser(operatorUser, data.UserId); + Cacheable cacheableChannel = GetCacheableDMChannel(channel, data.ChatCode); + Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, async () => + { + SocketDMChannel dmChannel; + if (channel is not null) + dmChannel = channel; + else + { + UserChat userChat = await ApiClient.GetUserChatAsync(data.ChatCode).ConfigureAwait(false); + dmChannel = CreateDMChannel(data.ChatCode, userChat.Recipient, State); + } + return await dmChannel.GetMessageAsync(data.MessageId).ConfigureAwait(false); + }); + SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, operatorUser); + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_directReactionAddedEvent, nameof(DirectReactionAdded), + cacheableMsg, cacheableChannel, cacheableUser, reaction).ConfigureAwait(false); + } + + /// + /// "PERSON", "private_deleted_reaction" + /// + private async Task HandlePrivateDeletedReaction(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + SocketDMChannel? channel = GetDMChannel(data.ChatCode); + SocketUserMessage? cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + SocketUser? operatorUser = GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketUser(operatorUser, data.UserId); + Cacheable cacheableChannel = GetCacheableDMChannel(channel, data.ChatCode); + Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, async () => + { + SocketDMChannel dmChannel; + if (channel is not null) + dmChannel = channel; + else + { + UserChat userChat = await ApiClient.GetUserChatAsync(data.ChatCode).ConfigureAwait(false); + dmChannel = CreateDMChannel(data.ChatCode, userChat.Recipient, State); + } + return await dmChannel.GetMessageAsync(data.MessageId).ConfigureAwait(false); + }); + SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, operatorUser); + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_directReactionRemovedEvent, nameof(DirectReactionRemoved), + cacheableMsg, cacheableChannel, cacheableUser, reaction).ConfigureAwait(false); + } + + #endregion + + #region Guild Members + + /// + /// "GROUP", "joined_guild" + /// + private async Task HandleJoinedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + SocketGuildUser? user = AlwaysDownloadUsers ? await DownloadUserAsync() : null; + Cacheable cacheableUser = new(user, data.UserId, user is not null, DownloadUserAsync); + guild.MemberCount++; + + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), + cacheableUser, data.JoinedAt).ConfigureAwait(false); + return; + + async Task DownloadUserAsync() + { + GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); + SocketGuildUser member = guild.AddOrUpdateUser(model); + return member; + } + } + + /// + /// "GROUP", "exited_guild" + /// + private async Task HandleExitedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + SocketUser? user = guild.RemoveUser(data.UserId) ?? State.GetUser(data.UserId) as SocketUser; + guild.MemberCount--; + GetCacheableSocketUser(user, data.UserId); + Cacheable cacheableUser = GetCacheableSocketUser(user, data.UserId); + + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), + guild, cacheableUser, data.ExitedAt).ConfigureAwait(false); + } + + /// + /// "GROUP", "updated_guild_member" + /// + private async Task HandleUpdatedGuildMember(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? user = guild.GetUser(data.UserId); + SocketGuildUser? before = user?.Clone(); + user?.Update(State, data); + Cacheable cacheableBefore = new(before, data.UserId, before is not null, + () => Task.FromResult(null)); + Cacheable cacheableAfter = GetCacheableSocketGuildUser(user, data.UserId, guild); + + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), + cacheableBefore, cacheableAfter).ConfigureAwait(false); + } + + /// + /// "PERSON", "guild_member_online" + /// + private async Task HandleGuildMemberOnline(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + List> users = []; + foreach (ulong guildId in data.CommonGuilds) + { + if (State.GetGuild(guildId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, guildId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketGuildUser? user = guild.GetUser(data.UserId); + user?.Presence.Update(true); + users.Add(new Cacheable(user, data.UserId, user is not null, + async () => + { + GuildMember model = await ApiClient + .GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); + SocketGuildUser member = guild.AddOrUpdateUser(model); + member.Presence.Update(true); + return user; + })); + } + + await TimedInvokeAsync(_guildMemberOnlineEvent, nameof(GuildMemberOnline), + users, data.EventTime).ConfigureAwait(false); + } + + /// + /// "PERSON", "guild_member_offline" + /// + private async Task HandleGuildMemberOffline(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + List> users = []; + foreach (ulong guildId in data.CommonGuilds) + { + if (State.GetGuild(guildId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, guildId, gatewayEvent).ConfigureAwait(false); + return; + } + + SocketGuildUser? user = guild.GetUser(data.UserId); + user?.Presence.Update(false); + users.Add(new Cacheable(user, data.UserId, user is not null, + async () => + { + GuildMember model = await ApiClient + .GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); + SocketGuildUser member = guild.AddOrUpdateUser(model); + member.Presence.Update(false); + return user; + })); + } + + await TimedInvokeAsync(_guildMemberOnlineEvent, nameof(GuildMemberOffline), + users, data.EventTime).ConfigureAwait(false); + } + + #endregion + + #region Guild Roles + + /// + /// "GROUP", "added_role" + /// + private async Task HandleAddedRole(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketRole role = guild.AddRole(data); + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_role" + /// + private async Task HandleDeletedRole(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketRole role = guild.RemoveRole(data.Id) ?? SocketRole.Create(guild, State, data); + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); + } + + /// + /// "GROUP", "updated_role" + /// + private async Task HandleUpdatedRole(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketRole? role = guild.GetRole(data.Id); + if (role is null) + { + await UnknownRoleAsync(gatewayEvent.ExtraData.Type, data.Id, guild.Id, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketRole before = role.Clone(); + role.Update(State, data); + + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); + } + + #endregion + + #region Guild Emojis + + /// + /// "GROUP", "added_emoji" + /// + private async Task HandleAddedRmoji(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + GuildEmote emote = guild.AddOrUpdateEmote(data); + + await TimedInvokeAsync(_emoteCreatedEvent, nameof(EmoteCreated), emote, guild).ConfigureAwait(false); + } + + /// + /// "GROUP", "updated_emoji" + /// + private async Task HandleUpdatedEmoji(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + GuildEmote? emote = guild.GetEmote(data.Id); + GuildEmote? before = emote?.Clone(); + GuildEmote after = guild.AddOrUpdateEmote(data); + + await TimedInvokeAsync(_emoteUpdatedEvent, nameof(EmoteUpdated), before, after, guild).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_emoji" + /// + private async Task HandleDeletedEmoji(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + GuildEmote emote = guild.RemoveEmote(data.Id) + ?? new GuildEmote(data.Id, data.Name, data.Type is EmojiType.Animated, guild.Id, null); + await TimedInvokeAsync(_emoteDeletedEvent, nameof(EmoteDeleted), emote, guild).ConfigureAwait(false); + } + + #endregion + + #region Guilds + + /// + /// "GROUP", "updated_guild" + /// + private async Task HandleUpdatedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuild before = guild.Clone(); + guild.Update(State, data); + if (AlwaysDownloadBoostSubscriptions + && (before.BoostSubscriptionCount != guild.BoostSubscriptionCount + || before.BufferBoostSubscriptionCount != guild.BufferBoostSubscriptionCount)) + await guild.DownloadBoostSubscriptionsAsync().ConfigureAwait(false); + + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_guild" + /// + private async Task HandleDeletedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is null) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + await GuildUnavailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + ((IDisposable)guild).Dispose(); + } + + /// + /// "GROUP", "added_block_list" + /// + private async Task HandleAddedBlockList(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? operatorUser = guild.GetUser(data.OperatorUserId); + Cacheable cacheableOperatorUser = + GetCacheableSocketGuildUser(operatorUser, data.OperatorUserId, guild); + IReadOnlyCollection> bannedUsers = data.UserIds + .Select(userId => GetCacheableSocketUser(guild.GetUser(userId), userId)) + .ToImmutableArray(); + + await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), + bannedUsers, cacheableOperatorUser, guild, data.Reason).ConfigureAwait(false); + } + + /// + /// "GROUP", "deleted_block_list" + /// + private async Task HandleDeletedBlockList(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? operatorUser = guild.GetUser(data.OperatorUserId); + Cacheable cacheableOperatorUser = + GetCacheableSocketGuildUser(operatorUser, data.OperatorUserId, guild); + IReadOnlyCollection> bannedUsers = data.UserIds + .Select(userId => GetCacheableSocketUser(guild.GetUser(userId), userId)) + .ToImmutableArray(); + + await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), + bannedUsers, cacheableOperatorUser, guild).ConfigureAwait(false); + } + + #endregion + + #region Users + + /// + /// "GROUP", "joined_channel" + /// + private async Task HandleJoinedChannel(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + if (GetChannel(data.ChannelId) is not SocketVoiceChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? user = guild.GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketGuildUser(user, data.UserId, guild); + guild.AddOrUpdateVoiceState(data.UserId, channel.Id); + + await TimedInvokeAsync(_userConnectedEvent, nameof(UserConnected), + cacheableUser, channel, data.At).ConfigureAwait(false); + } + + /// + /// "GROUP", "exited_channel" + /// + private async Task HandleExitedChannel(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.GetGuild(gatewayEvent.TargetId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + if (GetChannel(data.ChannelId) is not SocketVoiceChannel channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, data.ChannelId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? user = guild.GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketGuildUser(user, data.UserId, guild); + guild.AddOrUpdateVoiceState(data.UserId, null); + + await TimedInvokeAsync(_userDisconnectedEvent, nameof(UserDisconnected), + cacheableUser, channel, data.At).ConfigureAwait(false); + } + + /// + /// "PERSON", "user_updated" + /// + private async Task HandleUserUpdated(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (data.UserId == CurrentUser?.Id) + { + SocketSelfUser selfBefore = CurrentUser.Clone(); + CurrentUser.Update(State, data); + await TimedInvokeAsync(_currentUserUpdatedEvent, nameof(CurrentUserUpdated), + selfBefore, CurrentUser).ConfigureAwait(false); + return; + } + + SocketUser? user = GetUser(data.UserId); + SocketUser? before = user?.Clone(); + user?.Update(State, data); + Cacheable cacheableBefore = new(before, data.UserId, before is not null, + () => Task.FromResult(null)); + Cacheable cacheableUser = GetCacheableSocketUser(user, data.UserId); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), + cacheableBefore, cacheableUser).ConfigureAwait(false); + } + + /// + /// "PERSON", "self_joined_guild" + /// + private async Task HandleSelfJoinedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + await Task.Yield(); + _ = Task.Run(async () => + { + try + { + int remainingRetryTimes = BaseConfig.MaxJoinedGuildDataFetchingRetryTimes; + while (true) + { + try + { + return await ApiClient.GetGuildAsync(data.GuildId).ConfigureAwait(false); + } + catch (HttpException ex) + when (ex is { HttpCode: HttpStatusCode.OK, KookCode: KookErrorCode.GeneralError }) + { + if (remainingRetryTimes < 0) + throw; + } + + double retryDelay = BaseConfig.JoinedGuildDataFetchingRetryDelay / 1000D; + await _gatewayLogger + .WarningAsync($"Failed to get guild {data.GuildId} after joining. " + + $"Retrying in {retryDelay:F3} second{(retryDelay is 1 ? string.Empty : "s")} " + + $"for {remainingRetryTimes} more time{(remainingRetryTimes is 1 ? string.Empty : "s")}.") + .ConfigureAwait(false); + remainingRetryTimes--; + await Task.Delay(TimeSpan.FromMilliseconds(retryDelay)).ConfigureAwait(false); + } + } + catch (Exception e) + { + string payloadJson = SerializePayload(gatewayEvent); + await _gatewayLogger + .ErrorAsync($"Error handling {gatewayEvent.ExtraData.Type}. Payload: {payloadJson}", e) + .ConfigureAwait(false); + throw; + } + }).ContinueWith(async x => + { + ExtendedGuild model = x.Result; + SocketGuild guild = AddGuild(model, State); + guild.Update(State, model); + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + + /// + /// "PERSON", "self_exited_guild" + /// + private async Task HandleSelfExitedGuild(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (State.RemoveGuild(data.GuildId) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, data.GuildId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + await GuildUnavailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + ((IDisposable)guild).Dispose(); + } + + #endregion + + #region Interactions + + /// + /// "PERSON", "message_btn_click" + /// + private async Task HandleMessageButtonClick(GatewayEvent gatewayEvent) + { + if (DeserializePayload(gatewayEvent.ExtraData.Body) is not { } data) return; + if (data.GuildId.HasValue) + { + if (GetGuild(data.GuildId.Value) is not { } guild) + { + await UnknownGuildAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + if (guild.GetTextChannel(data.ChannelId) is not { } channel) + { + await UnknownChannelAsync(gatewayEvent.ExtraData.Type, gatewayEvent.TargetId, gatewayEvent) + .ConfigureAwait(false); + return; + } + + SocketGuildUser? user = channel.GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketGuildUser(user, data.UserId, guild); + SocketMessage? cachedMsg = channel.GetCachedMessage(data.MessageId); + Cacheable cacheableMessage = GetCacheableSocketMessage(cachedMsg, data.MessageId, channel); + + await TimedInvokeAsync(_messageButtonClickedEvent, nameof(MessageButtonClicked), + data.Value, cacheableUser, cacheableMessage, channel).ConfigureAwait(false); + } + else + { + SocketUser? user = GetUser(data.UserId); + Cacheable cacheableUser = GetCacheableSocketUser(user, data.UserId); + SocketDMChannel? channel = GetDMChannel(data.UserId); + if (channel is null) + { + CreateUserChatParams createUserChatParams = new() + { + UserId = data.UserId + }; + UserChat model = await ApiClient.CreateUserChatAsync(createUserChatParams).ConfigureAwait(false); + channel = CreateDMChannel(model.Code, model.Recipient, State); + } + Cacheable cacheableMessage = GetCacheableSocketMessage(null, data.MessageId, channel); + await TimedInvokeAsync(_directMessageButtonClickedEvent, nameof(DirectMessageButtonClicked), + data.Value, cacheableUser, cacheableMessage, channel).ConfigureAwait(false); + } + } + + #endregion + + #region Cacheable + + private Cacheable GetCacheableSocketUser(SocketUser? value, ulong id) + { + return new Cacheable(value, id, value is not null, + async () => + { + User model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); + return State.AddOrUpdateUser(id, + _ => SocketGlobalUser.Create(this, State, model), + (_, x) => + { + x.Update(State, model); + x.UpdatePresence(model.Online, model.OperatingSystem); + return x; + }); + }); + } + + private Cacheable GetCacheableSocketGuildUser(SocketGuildUser? value, + ulong id, SocketGuild guild) + { + return new Cacheable(value, id, value is not null, + async () => + { + GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, id).ConfigureAwait(false); + return guild.AddOrUpdateUser(model); + }); + } + + private Cacheable GetCacheableDMChannel(SocketDMChannel? value, + Guid chatCode, ulong userId) + { + return new Cacheable(value, chatCode, value is not null, + async () => + { + User user = await ApiClient.GetUserAsync(userId).ConfigureAwait(false); + return CreateDMChannel(chatCode, user, State); + }); + } + + private Cacheable GetCacheableDMChannel(SocketDMChannel? value, Guid chatCode) + { + return new Cacheable(value, chatCode, value is not null, + async () => + { + UserChat userChat = await ApiClient.GetUserChatAsync(chatCode).ConfigureAwait(false); + return CreateDMChannel(chatCode, userChat.Recipient, State); + }); + } + + private Cacheable GetCacheableSocketMessage(SocketMessage? value, + Guid messageId, IMessageChannel channel) + { + return new Cacheable(value, messageId, value is not null, + async () => await channel.GetMessageAsync(messageId).ConfigureAwait(false) as SocketMessage); + } + + #endregion + + #region Helpers + + private T? DeserializePayload(JsonElement jsonElement) + { + if (jsonElement.Deserialize(_serializerOptions) is { } x) return x; + string payloadJson = SerializePayload(jsonElement); + _gatewayLogger.ErrorAsync($"Failed to deserialize JSON element to type {typeof(T).Name}: {payloadJson}"); + return default; + } + + private string SerializePayload(object jsonElement) => + JsonSerializer.Serialize(jsonElement, _serializerOptions); + + #endregion + +} diff --git a/src/Kook.Net.WebSocket/KookSocketClient.cs b/src/Kook.Net.WebSocket/KookSocketClient.cs index e91dcb13..d0ed0f1c 100644 --- a/src/Kook.Net.WebSocket/KookSocketClient.cs +++ b/src/Kook.Net.WebSocket/KookSocketClient.cs @@ -12,7 +12,6 @@ using Kook.Net.Udp; using Kook.Net.WebSockets; using Kook.Rest; -using Reaction = Kook.API.Gateway.Reaction; namespace Kook.WebSocket; @@ -33,7 +32,8 @@ public partial class KookSocketClient : BaseSocketClient, IKookClient private Guid? _sessionId; private int _lastSeq; private int _retryCount; - private Task _heartbeatTask, _guildDownloadTask; + private Task? _heartbeatTask; + private Task? _guildDownloadTask; private int _unavailableGuildCount; private long _lastGuildAvailableTime, _lastMessageTime; private int _nextAudioId; @@ -78,8 +78,8 @@ public partial class KookSocketClient : BaseSocketClient, IKookClient /// /// A collection of DM channels that have been opened in this session. /// - public IReadOnlyCollection DMChannels - => State.DMChannels.Where(x => x is not null).ToImmutableArray(); + public IReadOnlyCollection DMChannels => + State.DMChannels.Where(x => x is not null).ToImmutableArray(); /// /// Initializes a new REST/WebSocket-based Kook client. @@ -119,7 +119,8 @@ private KookSocketClient(KookSocketConfig config, KookSocketApiClient client) _serializerOptions = new JsonSerializerOptions { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; ApiClient.SentGatewayMessage += async socketFrameType => @@ -148,8 +149,8 @@ private KookSocketClient(KookSocketConfig config, KookSocketApiClient client) }; } - private static KookSocketApiClient CreateApiClient(KookSocketConfig config) - => new(config.RestClientProvider, config.WebSocketProvider, KookConfig.UserAgent, + private static KookSocketApiClient CreateApiClient(KookSocketConfig config) => + new(config.RestClientProvider, config.WebSocketProvider, KookConfig.UserAgent, config.AcceptLanguage, config.GatewayHost, defaultRatelimitCallback: config.DefaultRatelimitCallback); internal override void Dispose(bool disposing) @@ -212,13 +213,13 @@ private async Task OnDisconnectingAsync(Exception ex) //Wait for tasks to complete await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); - Task heartbeatTask = _heartbeatTask; - if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); - + Task? heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - while (_heartbeatTimes.TryDequeue(out _)) { + // flush the queue } _lastMessageTime = 0; @@ -231,20 +232,16 @@ private async Task OnDisconnectingAsync(Exception ex) } /// - public override SocketGuild GetGuild(ulong id) - => State.GetGuild(id); + public override SocketGuild? GetGuild(ulong id) => State.GetGuild(id); /// - public override SocketChannel GetChannel(ulong id) - => State.GetChannel(id); + public override SocketChannel? GetChannel(ulong id) => State.GetChannel(id); /// - public override SocketDMChannel GetDMChannel(Guid chatCode) - => State.GetDMChannel(chatCode); + public override SocketDMChannel? GetDMChannel(Guid chatCode) => State.GetDMChannel(chatCode); /// - public override SocketDMChannel GetDMChannel(ulong userId) - => State.GetDMChannel(userId); + public override SocketDMChannel? GetDMChannel(ulong userId) => State.GetDMChannel(userId); /// /// Gets a generic channel from the cache or does a rest request if unavailable. @@ -255,8 +252,11 @@ public override SocketDMChannel GetDMChannel(ulong userId) /// A task that represents the asynchronous get operation. The task result contains the channel associated /// with the identifier; null when the channel cannot be found. /// - public async Task GetChannelAsync(ulong id, RequestOptions options = null) - => GetChannel(id) ?? (IChannel)await ClientHelper.GetChannelAsync(this, id, options).ConfigureAwait(false); + public async Task GetChannelAsync(ulong id, RequestOptions? options = null) + { + if (GetChannel(id) is { } channel) return channel; + return await ClientHelper.GetChannelAsync(this, id, options).ConfigureAwait(false); + } /// /// Gets a direct message channel from the cache or does a rest request if unavailable. @@ -267,8 +267,8 @@ public async Task GetChannelAsync(ulong id, RequestOptions options = n /// A task that represents the asynchronous get operation. The task result contains the channel associated /// with the identifier; null when the channel cannot be found. /// - public async Task GetDMChannelAsync(Guid chatCode, RequestOptions options = null) - => await ClientHelper.GetDMChannelAsync(this, chatCode, options).ConfigureAwait(false); + public async Task GetDMChannelAsync(Guid chatCode, RequestOptions? options = null) => + await ClientHelper.GetDMChannelAsync(this, chatCode, options).ConfigureAwait(false); /// /// Gets a collection of direct message channels from the cache or does a rest request if unavailable. @@ -278,8 +278,8 @@ public async Task GetDMChannelAsync(Guid chatCode, RequestOptions op /// A task that represents the asynchronous get operation. The task result contains the channel associated /// with the identifier; null when the channel cannot be found. /// - public async Task> GetDMChannelsAsync(RequestOptions options = null) - => (await ClientHelper.GetDMChannelsAsync(this, options).ConfigureAwait(false)).ToImmutableArray(); + public async Task> GetDMChannelsAsync(RequestOptions? options = null) => + (await ClientHelper.GetDMChannelsAsync(this, options).ConfigureAwait(false)).ToImmutableArray(); /// /// Gets a user from the cache or does a rest request if unavailable. @@ -290,16 +290,18 @@ public async Task> GetDMChannelsAsync(RequestOpt /// A task that represents the asynchronous get operation. The task result contains the user associated with /// the identifier; null if the user is not found. /// - public async Task GetUserAsync(ulong id, RequestOptions options = null) - => await ((IKookClient)this).GetUserAsync(id, CacheMode.AllowDownload, options).ConfigureAwait(false); + public async Task GetUserAsync(ulong id, RequestOptions? options = null) + { + if (GetUser(id) is { } user) return user; + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } /// - public override SocketUser GetUser(ulong id) - => State.GetUser(id); + public override SocketUser? GetUser(ulong id) => State.GetUser(id); /// - public override SocketUser GetUser(string username, string identifyNumber) - => State.Users.FirstOrDefault(x => x.IdentifyNumber == identifyNumber && x.Username == username); + public override SocketUser? GetUser(string username, string identifyNumber) => + State.Users.FirstOrDefault(x => x.IdentifyNumber == identifyNumber && x.Username == username); internal SocketGlobalUser GetOrCreateUser(ClientState state, User model) => state.GetOrAddUser(model.Id, _ => SocketGlobalUser.Create(this, state, model)); @@ -315,8 +317,7 @@ internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, User model) => return user; }); - internal void RemoveUser(ulong id) - => State.RemoveUser(id); + internal void RemoveUser(ulong id) => State.RemoveUser(id); /// /// Downloads all users for the specified guilds. @@ -325,20 +326,24 @@ internal void RemoveUser(ulong id) /// The guilds to download the users for. If null, all available guilds will be downloaded. /// /// The options to be used when sending the request. - public override async Task DownloadUsersAsync(IEnumerable guilds = null, RequestOptions options = null) + public override async Task DownloadUsersAsync(IEnumerable? guilds = null, RequestOptions? options = null) { - if (ConnectionState == ConnectionState.Connected) - await ProcessUserDownloadsAsync((guilds ?? Guilds.Where(x => x.IsAvailable)).Select(x => GetGuild(x.Id)).Where(x => x != null), options) - .ConfigureAwait(false); + if (ConnectionState != ConnectionState.Connected) return; + IEnumerable socketGuilds = (guilds ?? Guilds.Where(x => x.IsAvailable)) + .Select(x => GetGuild(x.Id)) + .OfType(); + await ProcessUserDownloadsAsync(socketGuilds, options).ConfigureAwait(false); } - private async Task ProcessUserDownloadsAsync(IEnumerable guilds, RequestOptions options) + private async Task ProcessUserDownloadsAsync(IEnumerable guilds, RequestOptions? options) { foreach (SocketGuild socketGuild in guilds) { - IEnumerable guildMembers = - await ApiClient.GetGuildMembersAsync(socketGuild.Id, options: options).FlattenAsync().ConfigureAwait(false); - socketGuild.Update(State, guildMembers.ToImmutableArray()); + IEnumerable guildMembers = await ApiClient + .GetGuildMembersAsync(socketGuild.Id, options: options) + .FlattenAsync() + .ConfigureAwait(false); + socketGuild.Update(State, [..guildMembers]); } } @@ -349,24 +354,32 @@ private async Task ProcessUserDownloadsAsync(IEnumerable guilds, Re /// The guilds to download the voice states for. If null, all available guilds will be downloaded. /// /// The options to be used when sending the request. - public override async Task DownloadVoiceStatesAsync(IEnumerable guilds = null, RequestOptions options = null) + public override async Task DownloadVoiceStatesAsync(IEnumerable? guilds = null, + RequestOptions? options = null) { - if (ConnectionState == ConnectionState.Connected) - await ProcessVoiceStateDownloadsAsync((guilds ?? Guilds.Where(x => x.IsAvailable)).Select(x => GetGuild(x.Id)).Where(x => x != null), - options).ConfigureAwait(false); + if (ConnectionState != ConnectionState.Connected) return; + IEnumerable socketGuilds = (guilds ?? Guilds.Where(x => x.IsAvailable)) + .Select(x => GetGuild(x.Id)) + .OfType(); + await ProcessVoiceStateDownloadsAsync(socketGuilds, options).ConfigureAwait(false); } - private async Task ProcessVoiceStateDownloadsAsync(IEnumerable guilds, RequestOptions options) + private async Task ProcessVoiceStateDownloadsAsync(IEnumerable guilds, RequestOptions? options) { foreach (SocketGuild socketGuild in guilds) { foreach (ulong channelId in socketGuild.VoiceChannels.Select(x => x.Id)) { - IReadOnlyCollection users = await ApiClient.GetConnectedUsersAsync(channelId, options).ConfigureAwait(false); - foreach (User user in users) socketGuild.AddOrUpdateVoiceState(user.Id, channelId); + IReadOnlyCollection users = await ApiClient + .GetConnectedUsersAsync(channelId, options) + .ConfigureAwait(false); + foreach (User user in users) + socketGuild.AddOrUpdateVoiceState(user.Id, channelId); } - GetGuildMuteDeafListResponse model = await ApiClient.GetGuildMutedDeafenedUsersAsync(socketGuild.Id).ConfigureAwait(false); + GetGuildMuteDeafListResponse model = await ApiClient + .GetGuildMutedDeafenedUsersAsync(socketGuild.Id) + .ConfigureAwait(false); foreach (ulong id in model.Muted.UserIds) socketGuild.AddOrUpdateVoiceState(id, true); foreach (ulong id in socketGuild.Users.Select(x => x.Id).Except(model.Deafened.UserIds)) @@ -387,28 +400,33 @@ private async Task ProcessVoiceStateDownloadsAsync(IEnumerable guil /// permission. /// /// The options to be used when sending the request. - public override async Task DownloadBoostSubscriptionsAsync(IEnumerable guilds = null, RequestOptions options = null) + public override async Task DownloadBoostSubscriptionsAsync(IEnumerable? guilds = null, + RequestOptions? options = null) { - if (ConnectionState == ConnectionState.Connected) - await ProcessBoostSubscriptionsDownloadsAsync( - (guilds ?? Guilds.Where(x => x.IsAvailable)).Select(x => GetGuild(x.Id)).Where(x => x != null), options).ConfigureAwait(false); + if (ConnectionState != ConnectionState.Connected) return; + IEnumerable socketGuilds = (guilds ?? Guilds.Where(x => x.IsAvailable)) + .Select(x => GetGuild(x.Id)) + .OfType(); + await ProcessBoostSubscriptionsDownloadsAsync(socketGuilds, options).ConfigureAwait(false); } - private async Task ProcessBoostSubscriptionsDownloadsAsync(IEnumerable guilds, RequestOptions options) + private async Task ProcessBoostSubscriptionsDownloadsAsync(IEnumerable guilds, RequestOptions? options) { foreach (SocketGuild socketGuild in guilds) { - IEnumerable subscriptions = await ApiClient.GetGuildBoostSubscriptionsAsync(socketGuild.Id, options: options) - .FlattenAsync().ConfigureAwait(false); - socketGuild.Update(State, subscriptions.ToImmutableArray()); + IEnumerable subscriptions = await ApiClient + .GetGuildBoostSubscriptionsAsync(socketGuild.Id, options: options) + .FlattenAsync() + .ConfigureAwait(false); + socketGuild.Update(State, [..subscriptions]); } } #region ProcessMessageAsync - private async Task ProcessMessageAsync(GatewaySocketFrameType gatewaySocketFrameType, int? sequence, object payload) + private async Task ProcessMessageAsync(GatewaySocketFrameType gatewaySocketFrameType, int? sequence, JsonElement payload) { - if (sequence != null) + if (sequence.HasValue) { if (sequence.Value != _lastSeq + 1) await _gatewayLogger.WarningAsync($"Missed a sequence number. Expected {_lastSeq + 1}, got {sequence.Value}."); @@ -422,21 +440,27 @@ private async Task ProcessMessageAsync(GatewaySocketFrameType gatewaySocketFrame switch (gatewaySocketFrameType) { case GatewaySocketFrameType.Event: - GatewayEvent gatewayEvent = - ((JsonElement)payload).Deserialize(_serializerOptions); + { + if (!payload.TryGetProperty("type", out JsonElement typeProperty) + || !typeProperty.TryGetInt32(out int typeValue) + || !Enum.IsDefined(typeof(MessageType), typeValue)) + { + await _gatewayLogger + .WarningAsync($"Unknown Event Type. Payload: {SerializePayload(payload)}") + .ConfigureAwait(false); + break; + } - dynamic eventExtraData = gatewayEvent!.Type switch + if (!payload.TryGetProperty("channel_type", out JsonElement channelTypeProperty) + || channelTypeProperty.GetString() is not { } channelType) { - MessageType.System => ((JsonElement)gatewayEvent.ExtraData) - .Deserialize(_serializerOptions), - _ when gatewayEvent.ChannelType == "GROUP" => ((JsonElement)gatewayEvent.ExtraData) - .Deserialize(_serializerOptions), - _ when gatewayEvent.ChannelType == "PERSON" => ((JsonElement)gatewayEvent.ExtraData) - .Deserialize(_serializerOptions), - _ => throw new InvalidOperationException("Unknown event type") - }; - - switch (gatewayEvent.Type) + await _gatewayLogger + .WarningAsync($"Unknown Channel Type. Payload: {SerializePayload(payload)}") + .ConfigureAwait(false); + break; + } + + switch ((MessageType)typeValue) { case MessageType.Text: case MessageType.Image: @@ -445,1447 +469,290 @@ private async Task ProcessMessageAsync(GatewaySocketFrameType gatewaySocketFrame case MessageType.Audio: case MessageType.KMarkdown: case MessageType.Card: + case MessageType.Poke: + { + await _gatewayLogger + .DebugAsync($"Received Message ({channelType}, {(MessageType)typeValue})") + .ConfigureAwait(false); + + switch (channelType) { - await _gatewayLogger.DebugAsync($"Received Message ({gatewayEvent.Type}, {gatewayEvent.ChannelType})") - .ConfigureAwait(false); - switch (gatewayEvent.ChannelType) + case "GROUP": { - case "GROUP" when eventExtraData is GatewayGroupMessageExtraData groupMessageExtraData: - { - SocketGuild guild = GetGuild(groupMessageExtraData.GuildId); - - if (guild is null) - { - await UnknownGuildAsync(gatewayEvent.ChannelType, groupMessageExtraData.GuildId, payload) - .ConfigureAwait(false); - return; - } - - SocketTextChannel channel = guild.GetTextChannel(gatewayEvent.TargetId); - SocketGuildUser author = guild.GetUser(groupMessageExtraData.Author.Id) ?? guild.AddOrUpdateUser(groupMessageExtraData.Author); - SocketMessage msg = SocketMessage.Create(this, State, author, channel, groupMessageExtraData, gatewayEvent); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg, author, channel).ConfigureAwait(false); - break; - } - case "PERSON" when eventExtraData is GatewayPersonMessageExtraData personMessageExtraData: - { - SocketUser author = State.GetOrAddUser(personMessageExtraData.Author.Id, - _ => SocketGlobalUser.Create(this, State, personMessageExtraData.Author)); - SocketDMChannel channel = GetDMChannel(personMessageExtraData.Code) - ?? AddDMChannel(personMessageExtraData.Code, personMessageExtraData.Author, State); - if (author == null) - { - await UnknownChannelUserAsync(gatewayEvent.Type.ToString(), personMessageExtraData.Author.Id, personMessageExtraData.Code, payload) - .ConfigureAwait(false); - return; - } - - SocketMessage msg = SocketMessage.Create(this, State, author, channel, personMessageExtraData, gatewayEvent); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_directMessageReceivedEvent, nameof(DirectMessageReceived), msg, author, channel).ConfigureAwait(false); - break; - } - default: - await _gatewayLogger.WarningAsync($"Unknown Event Type ({gatewayEvent.Type}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}") + GatewayEvent? gatewayEvent = + payload.Deserialize>(_serializerOptions); + if (gatewayEvent is null) + { + await _gatewayLogger + .WarningAsync($"Unable to deserialize System Group Message. Payload: {SerializePayload(payload)}") .ConfigureAwait(false); break; + } + await HandleGroupMessage(gatewayEvent).ConfigureAwait(false); } - } - break; - - case MessageType.Poke: - { - await _gatewayLogger.DebugAsync($"Received Message ({gatewayEvent.Type}, {gatewayEvent.ChannelType})") - .ConfigureAwait(false); - switch (gatewayEvent.ChannelType) + break; + case "PERSON": { - case "GROUP" when eventExtraData is GatewayGroupMessageExtraData groupMessageExtraData: - { - SocketChannel channel = GetChannel(gatewayEvent.TargetId); - - if (channel is not SocketTextChannel textChannel) - { - await UnknownChannelAsync(gatewayEvent.ChannelType, gatewayEvent.TargetId, payload) - .ConfigureAwait(false); - return; - } - - SocketGuild guild = textChannel.Guild; - SocketGuildUser author = guild.GetUser(groupMessageExtraData.Author.Id) ?? guild.AddOrUpdateUser(groupMessageExtraData.Author); - SocketMessage msg = SocketMessage.Create(this, State, author, textChannel, groupMessageExtraData, gatewayEvent); - SocketChannelHelper.AddMessage(textChannel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg, author, textChannel).ConfigureAwait(false); - break; - } - case "PERSON" when eventExtraData is GatewayPersonMessageExtraData personMessageExtraData: - { - SocketUser author = State.GetOrAddUser(personMessageExtraData.Author.Id, - _ => SocketGlobalUser.Create(this, State, personMessageExtraData.Author)); - SocketDMChannel channel = GetDMChannel(personMessageExtraData.Code) - ?? AddDMChannel(personMessageExtraData.Code, personMessageExtraData.Author, State); - if (author == null) - { - await UnknownChannelUserAsync(gatewayEvent.Type.ToString(), personMessageExtraData.Author.Id, personMessageExtraData.Code, payload) - .ConfigureAwait(false); - return; - } - - SocketMessage msg = SocketMessage.Create(this, State, author, channel, personMessageExtraData, gatewayEvent); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_directMessageReceivedEvent, nameof(DirectMessageReceived), msg, author, channel).ConfigureAwait(false); - break; - } - default: - await _gatewayLogger.WarningAsync($"Unknown Event Type ({gatewayEvent.Type}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}") + GatewayEvent? gatewayEvent = + payload.Deserialize>(_serializerOptions); + if (gatewayEvent is null) + { + await _gatewayLogger + .WarningAsync($"Unable to deserialize System Person Message. Payload: {SerializePayload(payload)}") .ConfigureAwait(false); break; + } + await HandlePersonMessage(gatewayEvent).ConfigureAwait(false); + } + break; + default: + { + await _gatewayLogger + .WarningAsync($"Unknown Channel Type ({channelType}). Payload: {SerializePayload(payload)}") + .ConfigureAwait(false); } + break; } + } break; - case MessageType.System: + { + GatewayEvent? gatewayEvent = + payload.Deserialize>(_serializerOptions); + if (gatewayEvent is not { ExtraData: { } extraData }) { - GatewaySystemEventExtraData extraData = eventExtraData as GatewaySystemEventExtraData; - switch (gatewayEvent.ChannelType, extraData!.Type) + await _gatewayLogger + .WarningAsync($"Unable to deserialize System Event. Payload: {SerializePayload(payload)}") + .ConfigureAwait(false); + break; + } + await _gatewayLogger + .DebugAsync($"Received Event ({channelType}, {extraData.Type})") + .ConfigureAwait(false); + switch (channelType, extraData.Type) + { + #region Channels + + // 频道内用户添加 reaction + case ("GROUP", "added_reaction"): + await HandleAddedReaction(gatewayEvent).ConfigureAwait(false); + break; + // 频道内用户取消 reaction + case ("GROUP", "deleted_reaction"): + await HandleDeletedReaction(gatewayEvent).ConfigureAwait(false); + break; + // 频道消息更新 + case ("GROUP", "updated_message"): + await HandleUpdatedMessage(gatewayEvent).ConfigureAwait(false); + break; + // 频道消息被删除 + case ("GROUP", "deleted_message"): + await HandleDeletedMessage(gatewayEvent).ConfigureAwait(false); + break; + // 新增频道 + case ("GROUP", "added_channel"): + await HandleAddedChannel(gatewayEvent).ConfigureAwait(false); + break; + // 修改频道信息 + case ("GROUP", "updated_channel"): + await HandleUpdatedChannel(gatewayEvent).ConfigureAwait(false); + break; + // 删除频道 + case ("GROUP", "deleted_channel"): + await HandleDeletedChannel(gatewayEvent).ConfigureAwait(false); + break; + // 新的频道置顶消息 + case ("GROUP", "pinned_message"): + await HandlePinnedMessage(gatewayEvent).ConfigureAwait(false); + break; + // 取消频道置顶消息 + case ("GROUP", "unpinned_message"): + await HandleUnpinnedMessage(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Direct Messages + + // 私聊消息更新 + case ("PERSON", "updated_private_message"): + await HandleUpdatedPrivateMessage(gatewayEvent).ConfigureAwait(false); + break; + // 私聊消息被删除 + case ("PERSON", "deleted_private_message"): + await HandleDeletedPrivateMessage(gatewayEvent).ConfigureAwait(false); + break; + // 私聊内用户添加 reaction + case ("PERSON", "private_added_reaction"): + await HandlePrivateAddedReaction(gatewayEvent).ConfigureAwait(false); + break; + // 私聊内用户取消 reaction + case ("PERSON", "private_deleted_reaction"): + await HandlePrivateDeletedReaction(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Guild Members + + // 新成员加入服务器 + case ("GROUP", "joined_guild"): + await HandleJoinedGuild(gatewayEvent).ConfigureAwait(false); + break; + // 服务器成员退出 + case ("GROUP", "exited_guild"): + await HandleExitedGuild(gatewayEvent).ConfigureAwait(false); + break; + // 服务器成员信息更新 + case ("GROUP", "updated_guild_member"): + await HandleUpdatedGuildMember(gatewayEvent).ConfigureAwait(false); + break; + // 服务器成员上线 + case ("PERSON", "guild_member_online"): + await HandleGuildMemberOnline(gatewayEvent).ConfigureAwait(false); + break; + // 服务器成员下线 + case ("PERSON", "guild_member_offline"): + await HandleGuildMemberOffline(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Guild Roles + + // 服务器角色增加 + case ("GROUP", "added_role"): + await HandleAddedRole(gatewayEvent).ConfigureAwait(false); + break; + // 服务器角色删除 + case ("GROUP", "deleted_role"): + await HandleDeletedRole(gatewayEvent).ConfigureAwait(false); + break; + // 服务器角色更新 + case ("GROUP", "updated_role"): + await HandleUpdatedRole(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Guild Emojis + + // 服务器表情新增 + case ("GROUP", "added_emoji"): + await HandleAddedRmoji(gatewayEvent).ConfigureAwait(false); + break; + // 服务器表情更新 + case ("GROUP", "updated_emoji"): + await HandleUpdatedEmoji(gatewayEvent).ConfigureAwait(false); + break; + // 服务器表情删除 + case ("GROUP", "deleted_emoji"): + await HandleDeletedEmoji(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Guilds + + // 服务器信息更新 + case ("GROUP", "updated_guild"): + await HandleUpdatedGuild(gatewayEvent).ConfigureAwait(false); + break; + // 服务器删除 + case ("GROUP", "deleted_guild"): + await HandleDeletedGuild(gatewayEvent).ConfigureAwait(false); + break; + // 服务器封禁用户 + case ("GROUP", "added_block_list"): + await HandleAddedBlockList(gatewayEvent).ConfigureAwait(false); + break; + // 服务器取消封禁用户 + case ("GROUP", "deleted_block_list"): + await HandleDeletedBlockList(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Users + + // 用户加入语音频道 + case ("GROUP", "joined_channel"): + await HandleJoinedChannel(gatewayEvent).ConfigureAwait(false); + break; + // 用户退出语音频道 + case ("GROUP", "exited_channel"): + await HandleExitedChannel(gatewayEvent).ConfigureAwait(false); + break; + // 用户信息更新 + case ("PERSON", "user_updated"): + await HandleUserUpdated(gatewayEvent).ConfigureAwait(false); + break; + // 自己新加入服务器 + case ("PERSON", "self_joined_guild"): + await HandleSelfJoinedGuild(gatewayEvent).ConfigureAwait(false); + break; + // 自己退出服务器 + case ("PERSON", "self_exited_guild"): + await HandleSelfExitedGuild(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + #region Interactions + + case ("PERSON", "message_btn_click"): + await HandleMessageButtonClick(gatewayEvent).ConfigureAwait(false); + break; + + #endregion + + default: { - #region Channels - - // 频道内用户添加 reaction - case ("GROUP", "added_reaction"): - { - await _gatewayLogger.DebugAsync("Received Event (added_reaction)").ConfigureAwait(false); - - Reaction data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketUser user = GetUser(data.UserId) ?? SocketUnknownUser.Create(this, State, data.UserId); - SocketGuildUser socketGuildUser = channel.GetUser(data.UserId); - Cacheable cacheableUser = - new(socketGuildUser, data.UserId, socketGuildUser != null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(channel.Guild.Id, data.UserId).ConfigureAwait(false); - return channel.Guild.AddOrUpdateUser(model); - }); - Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, user); - - cachedMsg?.AddReaction(reaction); - - await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, channel, - cacheableUser, reaction).ConfigureAwait(false); - } - break; - - // 频道内用户取消 reaction - case ("GROUP", "deleted_reaction"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_reaction)").ConfigureAwait(false); - - Reaction data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketUser user = GetUser(data.UserId) ?? SocketUnknownUser.Create(this, State, data.UserId); - SocketGuildUser socketGuildUser = channel.GetUser(data.UserId); - Cacheable cacheableUser = - new(socketGuildUser, data.UserId, socketGuildUser != null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(channel.Guild.Id, data.UserId).ConfigureAwait(false); - return channel.Guild.AddOrUpdateUser(model); - }); - Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, user); - - cachedMsg?.RemoveReaction(reaction); - - await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, channel, - cacheableUser, reaction).ConfigureAwait(false); - } - break; - - // 频道消息更新 - case ("GROUP", "updated_message"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_message)").ConfigureAwait(false); - MessageUpdateEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketGuild guild = channel.Guild; - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketMessage cachedMsg = channel.GetCachedMessage(data.MessageId); - SocketMessage before = cachedMsg?.Clone(); - cachedMsg?.Update(State, data); - Cacheable cacheableBefore = new(before, data.MessageId, cachedMsg != null, - () => Task.FromResult((SocketMessage)null)); - Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg != null, - async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as SocketMessage); - - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, cacheableAfter, - channel) - .ConfigureAwait(false); - } - break; - - // 频道消息被删除 - case ("GROUP", "deleted_message"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_message)").ConfigureAwait(false); - MessageDeleteEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketMessage msg = SocketChannelHelper.RemoveMessage(channel, this, data.MessageId); - Cacheable cacheableMsg = new(msg, data.MessageId, msg != null, - () => Task.FromResult((IMessage)null)); - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, channel) - .ConfigureAwait(false); - } - break; - - // 新增频道 - case ("GROUP", "added_channel"): - { - await _gatewayLogger.DebugAsync("Received Event (added_channel)").ConfigureAwait(false); - Channel data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, data.GuildId, payload) - .ConfigureAwait(false); - return; - } - - SocketChannel channel = guild.AddChannel(State, data); - await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); - } - break; - - // 修改频道信息 - case ("GROUP", "updated_channel"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_channel)").ConfigureAwait(false); - Channel data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketChannel channel = State.GetChannel(data.Id); - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, data.Id, payload) - .ConfigureAwait(false); - return; - } - - SocketChannel before = channel.Clone(); - channel.Update(State, data); - - await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); - } - break; - - // 删除频道 - case ("GROUP", "deleted_channel"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_channel)").ConfigureAwait(false); - ChannelDeleteEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketChannel channel = State.GetChannel(data.ChannelId); - - SocketGuild guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null) - channel = guild.RemoveChannel(State, data.ChannelId); - else - { - await UnknownGuildAsync(extraData.Type, 0, payload).ConfigureAwait(false); - return; - } - - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, guild.Id, payload).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); - } - break; - - // 新的频道置顶消息 - case ("GROUP", "pinned_message"): - { - await _gatewayLogger.DebugAsync("Received Event (pinned_message)").ConfigureAwait(false); - PinnedMessageEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketGuild guild = channel.Guild; - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketGuildUser operatorUser = guild.GetUser(data.OperatorUserId); - Cacheable cacheableOperatorUser = - new(operatorUser, data.OperatorUserId, operatorUser != null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(guild.Id, data.OperatorUserId).ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketMessage before = cachedMsg?.Clone(); - if (cachedMsg != null) - cachedMsg.IsPinned = true; - - Cacheable cacheableBefore = new(before, data.MessageId, before is not null, - () => Task.FromResult((SocketMessage)null)); - Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => await channel - .GetMessageAsync(data.MessageId).ConfigureAwait(false) as SocketMessage); - - await TimedInvokeAsync(_messagePinnedEvent, nameof(MessagePinned), cacheableBefore, cacheableAfter, - channel, cacheableOperatorUser).ConfigureAwait(false); - } - break; - - // 取消频道置顶消息 - case ("GROUP", "unpinned_message"): - { - await _gatewayLogger.DebugAsync("Received Event (unpinned_message)").ConfigureAwait(false); - UnpinnedMessageEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - if (GetChannel(data.ChannelId) is not SocketTextChannel channel) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload).ConfigureAwait(false); - return; - } - - SocketGuild guild = channel.Guild; - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketGuildUser operatorUser = guild.GetUser(data.OperatorUserId); - Cacheable cacheableOperatorUser = - new(operatorUser, data.OperatorUserId, operatorUser != null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(guild.Id, data.OperatorUserId).ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketMessage before = cachedMsg?.Clone(); - if (cachedMsg != null) - cachedMsg.IsPinned = false; - - Cacheable cacheableBefore = new(before, data.MessageId, before is not null, - () => Task.FromResult((SocketMessage)null)); - Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => await channel - .GetMessageAsync(data.MessageId).ConfigureAwait(false) as SocketMessage); - - await TimedInvokeAsync(_messageUnpinnedEvent, nameof(MessageUnpinned), cacheableBefore, cacheableAfter, - channel, cacheableOperatorUser).ConfigureAwait(false); - } - break; - - #endregion - - #region Direct Messages - - // 私聊消息更新 - case ("PERSON", "updated_private_message"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_private_message)").ConfigureAwait(false); - DirectMessageUpdateEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketDMChannel channel = GetDMChannel(data.ChatCode); - Cacheable cacheableChannel = new(channel, data.ChatCode, channel != null, - async () => { - User user = await ApiClient.GetUserAsync(data.AuthorId).ConfigureAwait(false); - return CreateDMChannel(data.ChatCode, user, State); - }); - - SocketMessage cachedMsg = channel?.GetCachedMessage(data.MessageId); - SocketMessage before = cachedMsg?.Clone(); - cachedMsg?.Update(State, data); - Cacheable cacheableBefore = new(before, data.MessageId, before is not null, - () => Task.FromResult((IMessage)null)); - SocketUser user = State.GetUser(data.AuthorId); - Cacheable cacheableAfter = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => - { - DirectMessage msg = await ApiClient.GetDirectMessageAsync(data.MessageId, data.ChatCode) - .ConfigureAwait(false); - SocketUser author = user ?? new SocketUnknownUser(this, data.AuthorId); - SocketMessage after = SocketMessage.Create(this, State, author, channel, msg); - return after; - }); - Cacheable cacheableUser = new(user, data.AuthorId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.AuthorId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.AuthorId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - await TimedInvokeAsync(_directMessageUpdatedEvent, nameof(DirectMessageUpdated), cacheableBefore, - cacheableAfter, cacheableUser, cacheableChannel).ConfigureAwait(false); - } - break; - - // 私聊消息被删除 - case ("PERSON", "deleted_private_message"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_private_message)").ConfigureAwait(false); - DirectMessageDeleteEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketDMChannel channel = GetDMChannel(data.ChatCode); - Cacheable cacheableChannel = new(channel, data.ChatCode, channel != null, - async () => { - User user = await ApiClient.GetUserAsync(data.AuthorId).ConfigureAwait(false); - return CreateDMChannel(data.ChatCode, user, State); - }); - - SocketMessage msg = null; - if (channel != null) msg = SocketChannelHelper.RemoveMessage(channel, this, data.MessageId); - - Cacheable cacheableMsg = new(msg, data.MessageId, msg != null, - () => Task.FromResult((IMessage)null)); - SocketUser user = State.GetUser(data.AuthorId); - Cacheable cacheableUser = new(user, data.AuthorId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.AuthorId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.AuthorId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - await TimedInvokeAsync(_directMessageDeletedEvent, nameof(DirectMessageDeleted), cacheableMsg, - cacheableUser, cacheableChannel).ConfigureAwait(false); - } - break; - - // 私聊内用户添加 reaction - case ("PERSON", "private_added_reaction"): - { - await _gatewayLogger.DebugAsync("Received Event (private_added_reaction)").ConfigureAwait(false); - - PrivateReaction data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketDMChannel channel = GetDMChannel(data.ChatCode); - SocketUserMessage cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketUser user = GetUser(data.UserId); - SocketUser operatorUser = user ?? SocketUnknownUser.Create(this, State, data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.UserId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.UserId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - Cacheable cacheableChannel = new(channel, data.ChatCode, channel is not null, - async () => await GetDMChannelAsync(data.ChatCode).ConfigureAwait(false)); - Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, async () => - { - IDMChannel channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false); - }); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, operatorUser); - - cachedMsg?.AddReaction(reaction); - - await TimedInvokeAsync(_directReactionAddedEvent, nameof(DirectReactionAdded), cacheableMsg, - cacheableChannel, cacheableUser, reaction).ConfigureAwait(false); - } - break; - - // 私聊内用户取消 reaction - case ("PERSON", "private_deleted_reaction"): - { - await _gatewayLogger.DebugAsync("Received Event (private_deleted_reaction)").ConfigureAwait(false); - - PrivateReaction data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketDMChannel channel = GetDMChannel(data.ChatCode); - SocketUserMessage cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - SocketUser user = GetUser(data.UserId); - SocketUser operatorUser = user ?? SocketUnknownUser.Create(this, State, data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.UserId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.UserId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - Cacheable cacheableChannel = new(channel, data.ChatCode, channel is not null, - async () => await GetDMChannelAsync(data.ChatCode).ConfigureAwait(false)); - Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, async () => - { - IDMChannel channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false); - }); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, operatorUser); - - cachedMsg?.RemoveReaction(reaction); - - await TimedInvokeAsync(_directReactionRemovedEvent, nameof(DirectReactionRemoved), cacheableMsg, - cacheableChannel, cacheableUser, reaction).ConfigureAwait(false); - } - break; - - #endregion - - #region Guild Members - - // 新成员加入服务器 - case ("GROUP", "joined_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (joined_guild)").ConfigureAwait(false); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildMemberAddEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); - SocketGuildUser user = guild.AddOrUpdateUser(model); - guild.MemberCount++; - await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user, data.JoinedAt) - .ConfigureAwait(false); - } - break; - - // 服务器成员退出 - case ("GROUP", "exited_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (exited_guild)").ConfigureAwait(false); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildMemberRemoveEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketUser user = guild.RemoveUser(data.UserId); - guild.MemberCount--; - user ??= State.GetUser(data.UserId); - - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.UserId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.UserId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - foreach (SocketGuildChannel channel in guild.Channels) - channel.RemoveUserPermissionOverwrite(data.UserId); - - await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, cacheableUser, data.ExitedAt) - .ConfigureAwait(false); - } - break; - - // 服务器成员信息更新 - case ("GROUP", "updated_guild_member"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_guild_member)").ConfigureAwait(false); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildMemberUpdateEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuildUser user = guild.GetUser(data.UserId); - - SocketGuildUser before = user?.Clone(); - user?.Update(State, data); - Cacheable cacheableBefore = new(before, data.UserId, before is not null, - () => Task.FromResult(null)); - Cacheable cacheableAfter = new(user, data.UserId, user is not null, - async () => - { - GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId) - .ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), - cacheableBefore, cacheableAfter).ConfigureAwait(false); - } - break; - - // 服务器成员上线 - case ("PERSON", "guild_member_online"): - { - await _gatewayLogger.DebugAsync("Received Event (guild_member_online)").ConfigureAwait(false); - GuildMemberOnlineEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - List> users = new(); - foreach (ulong guildId in data.CommonGuilds) - { - SocketGuild guild = State.GetGuild(guildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, guildId, payload).ConfigureAwait(false); - return; - } - - SocketGuildUser user = guild.GetUser(data.UserId); - user?.Presence.Update(true); - users.Add(new Cacheable(user, data.UserId, user is not null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(guild.Id, data.UserId) - .ConfigureAwait(false); - user = guild.AddOrUpdateUser(model); - user.Presence.Update(true); - return user; - })); - } - - await TimedInvokeAsync(_guildMemberOnlineEvent, nameof(GuildMemberOnline), users, data.OnlineAt) - .ConfigureAwait(false); - } - break; - - // 服务器成员下线 - case ("PERSON", "guild_member_offline"): - { - await _gatewayLogger.DebugAsync("Received Event (guild_member_offline)").ConfigureAwait(false); - GuildMemberOfflineEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - List> users = new(); - foreach (ulong guildId in data.CommonGuilds) - { - SocketGuild guild = State.GetGuild(guildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, guildId, payload).ConfigureAwait(false); - return; - } - - SocketGuildUser user = guild.GetUser(data.UserId); - user?.Presence.Update(false); - users.Add(new Cacheable(user, data.UserId, user is not null, - async () => - { - GuildMember model = await ApiClient - .GetGuildMemberAsync(guild.Id, data.UserId) - .ConfigureAwait(false); - user = guild.AddOrUpdateUser(model); - user.Presence.Update(false); - return user; - })); - } - - await TimedInvokeAsync(_guildMemberOfflineEvent, nameof(GuildMemberOffline), users, data.OfflineAt) - .ConfigureAwait(false); - } - break; - - #endregion - - #region Guild Roles - - // 服务器角色增加 - case ("GROUP", "added_role"): - { - await _gatewayLogger.DebugAsync("Received Event (added_role)").ConfigureAwait(false); - - Role data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketRole role = guild.AddRole(data); - await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); - } - break; - - // 服务器角色删除 - case ("GROUP", "deleted_role"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_role)").ConfigureAwait(false); - - Role data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - foreach (SocketGuildChannel channel in guild.Channels) - channel.RemoveRolePermissionOverwrite(data.Id); - - SocketRole role = guild.RemoveRole(data.Id); - await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); - } - break; - - // 服务器角色更新 - case ("GROUP", "updated_role"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_role)").ConfigureAwait(false); - - Role data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketRole role = guild.GetRole(data.Id); - if (role == null) - { - await UnknownRoleAsync(extraData.Type, data.Id, guild.Id, payload).ConfigureAwait(false); - return; - } - - SocketRole before = role.Clone(); - role.Update(State, data); - - await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role) - .ConfigureAwait(false); - } - break; - - #endregion - - #region Guild Emojis - - // 服务器表情新增 - case ("GROUP", "added_emoji"): - { - await _gatewayLogger.DebugAsync("Received Event (added_emoji)").ConfigureAwait(false); - - GuildEmojiEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildEmote emote = guild.AddEmote(data); - await TimedInvokeAsync(_emoteCreatedEvent, nameof(EmoteCreated), emote, guild).ConfigureAwait(false); - } - break; - - // 服务器表情更新 - case ("GROUP", "updated_emoji"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_emoji)").ConfigureAwait(false); - - GuildEmojiEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildEmote emote = guild.GetEmote(data?.Id); - GuildEmote before = emote.Clone(); - GuildEmote after = guild.AddOrUpdateEmote(data); - await TimedInvokeAsync(_emoteUpdatedEvent, nameof(EmoteUpdated), before, after, guild) - .ConfigureAwait(false); - } - break; - - // 服务器表情删除 - case ("GROUP", "deleted_emoji"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_emoji)").ConfigureAwait(false); - - GuildEmojiEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - GuildEmote emote = guild.RemoveEmote(data?.Id); - await TimedInvokeAsync(_emoteDeletedEvent, nameof(EmoteDeleted), emote, guild).ConfigureAwait(false); - } - break; - - #endregion - - #region Guilds - - // 服务器信息更新 - case ("GROUP", "updated_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (updated_guild)").ConfigureAwait(false); - - GuildEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, data.GuildId, payload).ConfigureAwait(false); - return; - } - - SocketGuild before = guild.Clone(); - guild.Update(State, data); - if (AlwaysDownloadBoostSubscriptions - && (before.BoostSubscriptionCount != guild.BoostSubscriptionCount - || before.BufferBoostSubscriptionCount != guild.BufferBoostSubscriptionCount)) - await guild.DownloadBoostSubscriptionsAsync().ConfigureAwait(false); - - await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); - } - break; - - // 服务器删除 - case ("GROUP", "deleted_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_guild)").ConfigureAwait(false); - GuildEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - SocketGuild guild = RemoveGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - await GuildUnavailableAsync(guild).ConfigureAwait(false); - await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); - ((IDisposable)guild).Dispose(); - } - break; - - // 服务器封禁用户 - case ("GROUP", "added_block_list"): - { - await _gatewayLogger.DebugAsync("Received Event (added_block_list)").ConfigureAwait(false); - GuildBanEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketUser operatorUser = guild.GetUser(data.OperatorUserId); - Cacheable cacheableOperatorUser = new(operatorUser, data.OperatorUserId, operatorUser != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.OperatorUserId).ConfigureAwait(false); - var operatorGlobalUser = State.GetOrAddUser(data.OperatorUserId, _ => SocketGlobalUser.Create(this, State, model)); - operatorGlobalUser.Update(State, model); - operatorGlobalUser.UpdatePresence(model.Online, model.OperatingSystem); - return operatorGlobalUser; - }); - IReadOnlyCollection> bannedUsers = data.UserIds.Select(id => - { - SocketUser bannedUser = guild.GetUser(id); - return new Cacheable(bannedUser, id, bannedUser != null, - async () => - { - User model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); - SocketGlobalUser bannedGlobalUser = State.GetOrAddUser(id, - _ => SocketGlobalUser.Create(this, State, model)); - bannedGlobalUser.Update(State, model); - bannedGlobalUser.UpdatePresence(model.Online, model.OperatingSystem); - return bannedGlobalUser; - }); - }).ToReadOnlyCollection(() => data.UserIds.Length); - await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), bannedUsers, cacheableOperatorUser, guild, data.Reason) - .ConfigureAwait(false); - } - break; - - // 服务器取消封禁用户 - case ("GROUP", "deleted_block_list"): - { - await _gatewayLogger.DebugAsync("Received Event (deleted_block_list)").ConfigureAwait(false); - GuildBanEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketUser operatorUser = guild.GetUser(data.OperatorUserId); - Cacheable cacheableOperatorUser = new(operatorUser, data.OperatorUserId, operatorUser != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.OperatorUserId).ConfigureAwait(false); - SocketGlobalUser operatorGlobalUser = State.GetOrAddUser(data.OperatorUserId, _ => SocketGlobalUser.Create(this, State, model)); - operatorGlobalUser.Update(State, model); - operatorGlobalUser.UpdatePresence(model.Online, model.OperatingSystem); - return operatorGlobalUser; - }); - IReadOnlyCollection> unbannedUsers = data.UserIds.Select(id => - { - SocketUser bannedUser = guild.GetUser(id); - return new Cacheable(bannedUser, id, bannedUser != null, - async () => - { - User model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); - SocketGlobalUser unbannedGlobalUser = State.GetOrAddUser(id, - _ => SocketGlobalUser.Create(this, State, model)); - unbannedGlobalUser.Update(State, model); - unbannedGlobalUser.UpdatePresence(model.Online, model.OperatingSystem); - return unbannedGlobalUser; - }); - }).ToReadOnlyCollection(() => data.UserIds.Length); - await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), unbannedUsers, cacheableOperatorUser, guild) - .ConfigureAwait(false); - } - break; - - #endregion - - #region Users - - // 用户加入语音频道 - case ("GROUP", "joined_channel"): - { - await _gatewayLogger.DebugAsync("Received Event (joined_channel)").ConfigureAwait(false); - UserVoiceEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload) - .ConfigureAwait(false); - return; - } - - SocketVoiceChannel channel = GetChannel(data.ChannelId) as SocketVoiceChannel; - - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload) - .ConfigureAwait(false); - return; - } - - SocketGuildUser user = guild.GetUser(data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - guild.AddOrUpdateVoiceState(data.UserId, channel.Id); - - await TimedInvokeAsync(_userConnectedEvent, nameof(UserConnected), cacheableUser, channel, data.At) - .ConfigureAwait(false); - } - break; - - // 用户退出语音频道 - case ("GROUP", "exited_channel"): - { - await _gatewayLogger.DebugAsync("Received Event (exited_channel)").ConfigureAwait(false); - UserVoiceEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - SocketGuild guild = State.GetGuild(gatewayEvent.TargetId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload) - .ConfigureAwait(false); - return; - } - - SocketVoiceChannel channel = GetChannel(data.ChannelId) as SocketVoiceChannel; - - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, data.ChannelId, payload) - .ConfigureAwait(false); - return; - } - - SocketGuildUser user = guild.GetUser(data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - guild.AddOrUpdateVoiceState(data.UserId, null); - - await TimedInvokeAsync(_userDisconnectedEvent, nameof(UserDisconnected), cacheableUser, channel, data.At) - .ConfigureAwait(false); - } - break; - - // 用户信息更新 - case ("PERSON", "user_updated"): - { - await _gatewayLogger.DebugAsync("Received Event (user_updated)").ConfigureAwait(false); - UserUpdateEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - if (data.UserId == CurrentUser.Id) - { - SocketSelfUser before = CurrentUser.Clone(); - CurrentUser.Update(State, data); - await TimedInvokeAsync(_currentUserUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser) - .ConfigureAwait(false); - } - else - { - SocketUser user = GetUser(data.UserId); - SocketUser before = user?.Clone(); - user?.Update(State, data); - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), - new Cacheable(before, data.UserId, before is not null, - () => Task.FromResult((SocketUser)null)), - new Cacheable(user, data.UserId, user is not null, - async () => - { - User model = await ApiClient.GetUserAsync(data.UserId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.UserId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - })) - .ConfigureAwait(false); - } - } - break; - - // 自己新加入服务器 - case ("PERSON", "self_joined_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (self_joined_guild)").ConfigureAwait(false); - SelfGuildEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - _ = Task.Run(async () => - { - try - { - int remainingRetryTimes = _baseConfig.MaxJoinedGuildDataFetchingRetryTimes; - while (true) - { - try - { - return await ApiClient.GetGuildAsync(data.GuildId) - .ConfigureAwait(false); - } - catch (HttpException ex) when (ex is - { - HttpCode: HttpStatusCode.OK, - KookCode: KookErrorCode.GeneralError - }) - { - if (remainingRetryTimes < 0) throw; - } - - await _gatewayLogger - .WarningAsync($"Failed to get guild {data.GuildId} after joining. Retrying in {_baseConfig.JoinedGuildDataFetchingRetryDelay:F3} second for {remainingRetryTimes} more times.") - .ConfigureAwait(false); - remainingRetryTimes--; - await Task.Delay(TimeSpan.FromMilliseconds(_baseConfig.JoinedGuildDataFetchingRetryDelay)).ConfigureAwait(false); - } - } - catch (Exception e) - { - await _gatewayLogger - .ErrorAsync($"Error handling {gatewaySocketFrameType}. Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}", e) - .ConfigureAwait(false); - return null; - } - }).ContinueWith(async t => - { - ExtendedGuild model = t.Result; - if (model is null) return; - SocketGuild guild = AddGuild(model, State); - guild.Update(State, model); - await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); - await GuildAvailableAsync(guild).ConfigureAwait(false); - }); - } - break; - - // 自己退出服务器 - case ("PERSON", "self_exited_guild"): - { - await _gatewayLogger.DebugAsync("Received Event (self_exited_guild)").ConfigureAwait(false); - SelfGuildEvent data = ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - - SocketGuild guild = RemoveGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - await GuildUnavailableAsync(guild).ConfigureAwait(false); - await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); - ((IDisposable)guild).Dispose(); - } - break; - - #endregion - - #region Interactions - - // Card 消息中的 Button 点击事件 - case ("PERSON", "message_btn_click"): - { - await _gatewayLogger.DebugAsync("Received Event (message_btn_click)").ConfigureAwait(false); - MessageButtonClickEvent data = - ((JsonElement)extraData.Body).Deserialize(_serializerOptions); - if (data.GuildId.HasValue) - { - SocketTextChannel channel = GetChannel(data.ChannelId) as SocketTextChannel; - SocketGuild guild = GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - SocketGuildUser user = channel.GetUser(data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - GuildMember model = await ApiClient.GetGuildMemberAsync(guild.Id, data.UserId).ConfigureAwait(false); - return guild.AddOrUpdateUser(model); - }); - - SocketMessage cachedMsg = channel.GetCachedMessage(data.MessageId); - Cacheable cacheableMsg = new(cachedMsg, data.MessageId, cachedMsg is not null, - async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)); - await TimedInvokeAsync(_messageButtonClickedEvent, nameof(MessageButtonClicked), - data.Value, cacheableUser, cacheableMsg, channel).ConfigureAwait(false); - } - else - { - SocketUser user = GetUser(data.UserId); - Cacheable cacheableUser = new(user, data.UserId, user != null, - async () => - { - User model = await ApiClient.GetUserAsync(data.UserId).ConfigureAwait(false); - SocketGlobalUser globalUser = State.GetOrAddUser(data.UserId, _ => SocketGlobalUser.Create(this, State, model)); - globalUser.Update(State, model); - globalUser.UpdatePresence(model.Online, model.OperatingSystem); - return globalUser; - }); - - - SocketDMChannel channel = GetDMChannel(data.UserId); - if (channel == null) - { - UserChat model = await ApiClient - .CreateUserChatAsync(new CreateUserChatParams() { UserId = data.UserId }).ConfigureAwait(false); - channel = CreateDMChannel(model.Code, model.Recipient, State); - } - - if (channel == null) - { - await UnknownChannelAsync(extraData.Type, gatewayEvent.TargetId, payload).ConfigureAwait(false); - return; - } - - Cacheable cacheableMsg = new(null, data.MessageId, false, - async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)); - await TimedInvokeAsync(_directMessageButtonClickedEvent, nameof(DirectMessageButtonClicked), - data.Value, cacheableUser, cacheableMsg, channel).ConfigureAwait(false); - } - } - break; - - #endregion - - default: - await _gatewayLogger.WarningAsync($"Unknown SystemEventType ({extraData.Type}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}") - .ConfigureAwait(false); - break; + await _gatewayLogger + .WarningAsync($"Unknown SystemEventType ({channelType}, {extraData.Type}). Payload: {SerializePayload(payload)}") + .ConfigureAwait(false); } + break; } + } break; default: - await _gatewayLogger.WarningAsync($"Unknown Event Type ({gatewayEvent.Type}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}") + { + await _gatewayLogger + .WarningAsync($"Unknown Event Type ({typeValue}). Payload: {SerializePayload(payload)}") .ConfigureAwait(false); + } break; } + } break; - case GatewaySocketFrameType.Hello: - { - // Process Hello - await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); - try - { - GatewayHelloPayload gatewayHelloPayload = - ((JsonElement)payload).Deserialize(_serializerOptions); - _sessionId = gatewayHelloPayload?.SessionId; - _heartbeatTask = RunHeartbeatAsync(_connection.CancellationToken); - } - catch (Exception ex) - { - _connection.CriticalError(new Exception("Processing Hello failed", ex)); - return; - } - - // Get current user - try - { - SelfUser selfUser = await ApiClient.GetSelfUserAsync().ConfigureAwait(false); - SocketSelfUser currentUser = SocketSelfUser.Create(this, State, selfUser); - Rest.CreateRestSelfUser(selfUser); - ApiClient.CurrentUserId = currentUser.Id; - Rest.CurrentUser = RestSelfUser.Create(this, selfUser); - CurrentUser = currentUser; - } - catch (Exception ex) - { - _connection.CriticalError(new Exception("Processing SelfUser failed", ex)); - return; - } - - // Download guild data - try - { - IReadOnlyCollection guilds = await ApiClient.ListGuildsAsync().ConfigureAwait(false); - ClientState state = new(guilds.Count, 0); - int unavailableGuilds = 0; - foreach (RichGuild guild in guilds) - { - RichGuild model = guild; - SocketGuild socketGuild = AddGuild(model, state); - if (!socketGuild.IsAvailable) - unavailableGuilds++; - else - await GuildAvailableAsync(socketGuild).ConfigureAwait(false); - } - - _unavailableGuildCount = unavailableGuilds; - State = state; - } - catch (Exception ex) - { - _connection.CriticalError(new Exception("Processing Guilds failed", ex)); - return; - } - - // // Download guild data - // try - // { - // var guilds = (await ApiClient.GetGuildsAsync().FlattenAsync().ConfigureAwait(false)).ToList(); - // var state = new ClientState(guilds.Count, 0); - // int unavailableGuilds = 0; - // foreach (Guild guild in guilds) - // { - // var model = guild; - // var socketGuild = AddGuild(model, state); - // if (!socketGuild.IsAvailable) - // unavailableGuilds++; - // else - // await GuildAvailableAsync(socketGuild).ConfigureAwait(false); - // } - // _unavailableGuildCount = unavailableGuilds; - // State = state; - // } - // catch (Exception ex) - // { - // _connection.CriticalError(new Exception("Processing Guilds failed", ex)); - // return; - // } - - _lastGuildAvailableTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _guildDownloadTask = WaitForGuildsAsync(_connection.CancellationToken, _gatewayLogger) - .ContinueWith(async task => - { - if (task.IsFaulted) - { - _connection.Error(task.Exception); - return; - } - else if (_connection.CancellationToken.IsCancellationRequested) return; - - // Download user list if enabled - if (_baseConfig.AlwaysDownloadUsers) - { - _ = Task.Run(async () => - { - try - { - await DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && x.HasAllMembers is not true)); - } - catch (Exception ex) - { - await _gatewayLogger.WarningAsync("Downloading users failed", ex).ConfigureAwait(false); - } - }); - } - - if (_baseConfig.AlwaysDownloadVoiceStates) - { - _ = Task.Run(async () => - { - try - { - await DownloadVoiceStatesAsync(Guilds.Where(x => x.IsAvailable)); - } - catch (Exception ex) - { - await _gatewayLogger.WarningAsync("Downloading voice states failed", ex).ConfigureAwait(false); - } - }); - } - - if (_baseConfig.AlwaysDownloadBoostSubscriptions) - { - _ = Task.Run(async () => - { - try - { - await DownloadBoostSubscriptionsAsync(Guilds.Where(x => x.IsAvailable)); - } - catch (Exception ex) - { - await _gatewayLogger.WarningAsync("Downloading boost subscriptions failed", ex).ConfigureAwait(false); - } - }); - } - - await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); - await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); - }); - - _ = _connection.CompleteAsync(); - } + await HandleGatewayHelloAsync(payload).ConfigureAwait(false); break; - case GatewaySocketFrameType.Pong: - { - await _gatewayLogger.DebugAsync("Received Pong").ConfigureAwait(false); - if (_heartbeatTimes.TryDequeue(out long time)) - { - int latency = (int)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - time); - int before = Latency; - Latency = latency; - - await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency) - .ConfigureAwait(false); - } - } + await HandlePongAsync().ConfigureAwait(false); break; - case GatewaySocketFrameType.Reconnect: - { - await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - GatewayReconnectPayload gatewayReconnectPayload = - ((JsonElement)payload).Deserialize(_serializerOptions); - if (gatewayReconnectPayload?.Code is KookErrorCode.MissingResumeArgument - or KookErrorCode.SessionExpired - or KookErrorCode.InvalidSequenceNumber) - { - _sessionId = null; - _lastSeq = 0; - } - - _connection.Error(new GatewayReconnectException($"Server requested a reconnect, resuming session failed" - + $"{(string.IsNullOrWhiteSpace(gatewayReconnectPayload?.Message) ? string.Empty : $": {gatewayReconnectPayload.Message}")}")); - } + await HandleReconnectAsync(payload).ConfigureAwait(false); break; - case GatewaySocketFrameType.ResumeAck: - { - await _gatewayLogger.DebugAsync("Received ResumeAck").ConfigureAwait(false); - _ = _connection.CompleteAsync(); - - //Notify the client that these guilds are available again - foreach (SocketGuild guild in State.Guilds) - if (guild.IsAvailable) - await GuildAvailableAsync(guild).ConfigureAwait(false); - - await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); - } + await HandleResumeAckAsync().ConfigureAwait(false); break; - default: - await _gatewayLogger.WarningAsync($"Unknown Socket Frame Type ({gatewaySocketFrameType}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}") + { + await _gatewayLogger + .WarningAsync($"Unknown Socket Frame Type ({gatewaySocketFrameType}). Payload: {SerializePayload(payload)}") .ConfigureAwait(false); + } break; } } catch (Exception ex) { - await _gatewayLogger.ErrorAsync($"Error handling {gatewaySocketFrameType}. Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}", ex).ConfigureAwait(false); + await _gatewayLogger + .ErrorAsync($"Error handling {gatewaySocketFrameType}. Payload: {SerializePayload(payload)}", ex) + .ConfigureAwait(false); } } @@ -1908,12 +775,12 @@ private async Task RunHeartbeatAsync(CancellationToken cancellationToken) long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); //Did server respond to our last heartbeat, or are we still receiving messages (long load?) - if (_heartbeatTimes.IsEmpty && now - _lastMessageTime > intervalMillis + 1000.0 / 64) - if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) - { - _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); - return; - } + if (_heartbeatTimes.IsEmpty && now - _lastMessageTime > intervalMillis + 1000.0 / 64 + && ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) + { + _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); + return; + } _heartbeatTimes.Enqueue(now); try @@ -1947,7 +814,7 @@ private async Task WaitForGuildsAsync(CancellationToken cancellationToken, Logge { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); while (_unavailableGuildCount != 0 - && DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _lastGuildAvailableTime < _baseConfig.MaxWaitBetweenGuildAvailablesBeforeReady) + && DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady) await Task.Delay(500, cancellationToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); @@ -1976,8 +843,7 @@ internal SocketGuild AddGuild(RichGuild model, ClientState state) return guild; } - internal SocketGuild RemoveGuild(ulong id) - => State.RemoveGuild(id); + internal SocketGuild? RemoveGuild(ulong id) => State.RemoveGuild(id); internal SocketDMChannel AddDMChannel(UserChat model, ClientState state) { @@ -1993,108 +859,101 @@ internal SocketDMChannel AddDMChannel(Guid chatCode, User model, ClientState sta return channel; } - internal SocketDMChannel CreateDMChannel(Guid chatCode, User model, ClientState state) => SocketDMChannel.Create(this, state, chatCode, model); - internal SocketDMChannel CreateDMChannel(Guid chatCode, SocketUser user, ClientState state) => new(this, chatCode, user); + internal SocketDMChannel CreateDMChannel(Guid chatCode, User model, ClientState state) => + SocketDMChannel.Create(this, state, chatCode, model); + + internal SocketDMChannel CreateDMChannel(Guid chatCode, SocketUser user, ClientState state) => + new(this, chatCode, user); private async Task GuildAvailableAsync(SocketGuild guild) { - if (!guild.IsConnected) - { - guild.IsConnected = true; - await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); - } + if (guild.IsConnected) return; + guild.IsConnected = true; + await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); } private async Task GuildUnavailableAsync(SocketGuild guild) { - if (guild.IsConnected) - { - guild.IsConnected = false; - await TimedInvokeAsync(_guildUnavailableEvent, nameof(GuildUnavailable), guild).ConfigureAwait(false); - } + if (!guild.IsConnected) return; + guild.IsConnected = false; + await TimedInvokeAsync(_guildUnavailableEvent, nameof(GuildUnavailable), guild).ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, eventHandler.InvokeAsync).ConfigureAwait(false); - else - await eventHandler.InvokeAsync().ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, eventHandler.InvokeAsync).ConfigureAwait(false); + else + await eventHandler.InvokeAsync().ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T arg) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); - else - await eventHandler.InvokeAsync(arg).ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg).ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); - else - await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); - else - await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); - else - await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); } private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { - if (eventHandler.HasSubscribers) - { - if (HandlerTimeout.HasValue) - await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)) - .ConfigureAwait(false); - else - await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); - } + if (!eventHandler.HasSubscribers) return; + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); } private async Task TimeoutWrap(string name, Func action) { try { - // ReSharper disable once PossibleInvalidOperationException + if (!HandlerTimeout.HasValue) + { + await action().ConfigureAwait(false); + return; + } + Task timeoutTask = Task.Delay(HandlerTimeout.Value); Task handlersTask = action(); if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) + { await _gatewayLogger.WarningAsync($"A {name} handler is blocking the gateway task.") .ConfigureAwait(false); + } await handlersTask.ConfigureAwait(false); //Ensure the handler completes } @@ -2108,43 +967,43 @@ await _gatewayLogger.WarningAsync($"A {name} handler has thrown an unhandled exc private async Task UnknownChannelUserAsync(string evnt, ulong userId, Guid chatCode, object payload) { string details = $"{evnt} User={userId} ChatCode={chatCode}"; - await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownGlobalUserAsync(string evnt, ulong userId, object payload) { string details = $"{evnt} User={userId}"; - await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId, object payload) { string details = $"{evnt} User={userId} Channel={channelId}"; - await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId, object payload) { string details = $"{evnt} User={userId} Guild={guildId}"; - await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown User ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId, object payload) { string details = $"{evnt} User={userId} Guild={guildId}"; - await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownPrivateChannelAsync(string evnt, Guid chatCode, object payload) { string details = $"{evnt} Channel={chatCode}"; - await _gatewayLogger.WarningAsync($"Unknown Private Channel ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Private Channel ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId, object payload) { string details = $"{evnt} Channel={channelId}"; - await _gatewayLogger.WarningAsync($"Unknown Channel ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId, object payload) @@ -2156,31 +1015,31 @@ private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guild } string details = $"{evnt} Channel={channelId} Guild={guildId}"; - await _gatewayLogger.WarningAsync($"Unknown Channel ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId, object payload) { string details = $"{evnt} Role={roleId} Guild={guildId}"; - await _gatewayLogger.WarningAsync($"Unknown Role ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Role ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownGuildAsync(string evnt, ulong guildId, object payload) { string details = $"{evnt} Guild={guildId}"; - await _gatewayLogger.WarningAsync($"Unknown Guild ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Guild ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnknownGuildEventAsync(string evnt, ulong eventId, ulong guildId, object payload) { string details = $"{evnt} Event={eventId} Guild={guildId}"; - await _gatewayLogger.WarningAsync($"Unknown Guild Event ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Guild Event ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } private async Task UnsyncedGuildAsync(string evnt, ulong guildId, object payload) { string details = $"{evnt} Guild={guildId}"; - await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}). Payload: {JsonSerializer.Serialize(payload, _serializerOptions)}").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}). Payload: {SerializePayload(payload)}").ConfigureAwait(false); } internal int GetAudioId() => _nextAudioId++; @@ -2188,33 +1047,38 @@ private async Task UnsyncedGuildAsync(string evnt, ulong guildId, object payload #region IKookClient /// - Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(Guilds); + Task> IKookClient.GetGuildsAsync(CacheMode mode, RequestOptions? options) => + Task.FromResult>(Guilds); /// - Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetGuild(id)); + Task IKookClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions? options) => + Task.FromResult(GetGuild(id)); /// - async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + async Task IKookClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) { - SocketUser user = GetUser(id); - if (user is not null || mode == CacheMode.CacheOnly) return user; - + if (GetUser(id) is { } user) + return user; + if (mode == CacheMode.CacheOnly) + return null; return await Rest.GetUserAsync(id, options).ConfigureAwait(false); } /// - async Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id); + async Task IKookClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id); /// - async Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetDMChannelAsync(chatCode, options).ConfigureAwait(false) : GetDMChannel(chatCode); + async Task IKookClient.GetDMChannelAsync(Guid chatCode, CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetDMChannelAsync(chatCode, options).ConfigureAwait(false) + : GetDMChannel(chatCode); /// - async Task> IKookClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetDMChannelsAsync(options).ConfigureAwait(false) : DMChannels; + async Task> IKookClient.GetDMChannelsAsync(CacheMode mode, RequestOptions? options) => + mode == CacheMode.AllowDownload + ? await GetDMChannelsAsync(options).ConfigureAwait(false) + : DMChannels; #endregion } diff --git a/src/Kook.Net.WebSocket/KookSocketConfig.cs b/src/Kook.Net.WebSocket/KookSocketConfig.cs index baef8f6c..1eaa8eb2 100644 --- a/src/Kook.Net.WebSocket/KookSocketConfig.cs +++ b/src/Kook.Net.WebSocket/KookSocketConfig.cs @@ -34,7 +34,7 @@ public class KookSocketConfig : KookRestConfig /// Gets or sets the WebSocket host to connect to. If null, the client will use the /// /gateway endpoint. /// - public string GatewayHost { get; set; } = null; + public string? GatewayHost { get; set; } /// /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. @@ -132,7 +132,6 @@ public class KookSocketConfig : KookRestConfig public int MaxWaitBetweenGuildAvailablesBeforeReady { get => _maxWaitForGuildAvailable; - set { Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady)); @@ -178,5 +177,5 @@ public KookSocketConfig() UdpSocketProvider = DefaultUdpSocketProvider.Instance; } - internal KookSocketConfig Clone() => MemberwiseClone() as KookSocketConfig; + internal KookSocketConfig Clone() => (KookSocketConfig)MemberwiseClone(); } diff --git a/src/Kook.Net.WebSocket/KookSocketRestClient.cs b/src/Kook.Net.WebSocket/KookSocketRestClient.cs index 75b71f1f..85d24ec1 100644 --- a/src/Kook.Net.WebSocket/KookSocketRestClient.cs +++ b/src/Kook.Net.WebSocket/KookSocketRestClient.cs @@ -15,20 +15,20 @@ internal KookSocketRestClient(KookRestConfig config, API.KookRestApiClient api) /// Throws a when trying to log in. /// /// The Socket REST wrapper cannot be used to log in or out. - public new Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) - => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + public new Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) => + throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); - internal override Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) - => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) => + throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); /// /// Throws a when trying to log out. /// /// /// The Socket REST wrapper cannot be used to log in or out. - public new Task LogoutAsync() - => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + public new Task LogoutAsync() => + throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); - internal override Task LogoutInternalAsync() - => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LogoutInternalAsync() => + throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); } diff --git a/src/Kook.Net.WebSocket/KookVoiceAPIClient.cs b/src/Kook.Net.WebSocket/KookVoiceAPIClient.cs index 16091822..ac1a7f99 100644 --- a/src/Kook.Net.WebSocket/KookVoiceAPIClient.cs +++ b/src/Kook.Net.WebSocket/KookVoiceAPIClient.cs @@ -2,12 +2,16 @@ using Kook.API.Voice; using Kook.Net.Udp; using Kook.Net.WebSockets; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +#if DEBUG_AUDIO +using System.Diagnostics; +#endif + namespace Kook.Audio; internal class KookVoiceAPIClient : IDisposable @@ -76,8 +80,9 @@ public event Func Disconnected private readonly ConcurrentDictionary _sequenceFrames; private readonly JsonSerializerOptions _serializerOptions; private readonly SemaphoreSlim _connectionLock; - private readonly IUdpSocket _udp, _rtcpUdp; - private CancellationTokenSource _connectCancellationToken; + private readonly IUdpSocket _udp; + private readonly IUdpSocket _rtcpUdp; + private CancellationTokenSource? _connectCancellationToken; private bool _isDisposed; public ulong GuildId { get; } @@ -89,7 +94,7 @@ public event Func Disconnected public ushort RtcpUdpPort => _rtcpUdp.Port; internal KookVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, - UdpSocketProvider udpSocketProvider, JsonSerializerOptions serializerOptions = null) + UdpSocketProvider udpSocketProvider, JsonSerializerOptions? serializerOptions = null) { GuildId = guildId; _sequenceFrames = new ConcurrentDictionary(); @@ -122,26 +127,22 @@ internal KookVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, WebSocketClient = webSocketProvider(); WebSocketClient.BinaryMessage += (data, index, count) => { - using (MemoryStream compressed = new(data, index + 2, count - 2)) - using (MemoryStream decompressed = new()) + using MemoryStream compressed = new(data, index + 2, count - 2); + using MemoryStream decompressed = new(); + using (DeflateStream zlib = new(compressed, CompressionMode.Decompress)) { - using (DeflateStream zlib = new(compressed, CompressionMode.Decompress)) - { - zlib.CopyTo(decompressed); - } - - decompressed.Position = 0; - using (StreamReader reader = new(decompressed)) - { - string json = reader.ReadToEnd(); - VoiceSocketIncomeFrame msg = JsonSerializer.Deserialize(json, serializerOptions); - return ProcessVoiceSocketFrame(msg); - } + zlib.CopyTo(decompressed); } + + decompressed.Position = 0; + using StreamReader reader = new(decompressed); + string json = reader.ReadToEnd(); + VoiceSocketIncomeFrame? msg = JsonSerializer.Deserialize(json, serializerOptions); + return ProcessVoiceSocketFrame(msg); }; WebSocketClient.TextMessage += text => { - VoiceSocketIncomeFrame msg = JsonSerializer.Deserialize(text, serializerOptions); + VoiceSocketIncomeFrame? msg = JsonSerializer.Deserialize(text, serializerOptions); return ProcessVoiceSocketFrame(msg); }; WebSocketClient.Closed += async ex => @@ -158,30 +159,30 @@ internal KookVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, }; } - private Task ProcessVoiceSocketFrame(VoiceSocketIncomeFrame msg) + private Task ProcessVoiceSocketFrame(VoiceSocketIncomeFrame? msg) { switch (msg) { case { Response: true } when _sequenceFrames.TryRemove(msg.Id, out VoiceSocketFrameType type): - { + { #if DEBUG_AUDIO - Debug.WriteLine($""" - <- [#{msg.Id}] [{type}] : [OK] {msg.Okay} - [Payload] {msg.Payload} - """); + Debug.WriteLine($""" + <- [#{msg.Id}] [{type}] : [OK] {msg.Okay} + [Payload] {msg.Payload} + """); #endif - return _receivedEvent.InvokeAsync(type, msg.Okay, msg.Payload); - } + return _receivedEvent.InvokeAsync(type, msg.Okay, msg.Payload); + } case { Notification: true }: - { + { #if DEBUG_AUDIO - Debug.WriteLine($""" - <- [Notification] [{msg.Method}] - [Data] {msg.Payload} - """); + Debug.WriteLine($""" + <- [Notification] [{msg.Method}] + [Data] {msg.Payload} + """); #endif - return _receivedEvent.InvokeAsync(msg.Method, true, msg.Payload); - } + return _receivedEvent.InvokeAsync(msg.Method, true, msg.Payload); + } } return Task.CompletedTask; @@ -189,33 +190,37 @@ private Task ProcessVoiceSocketFrame(VoiceSocketIncomeFrame msg) private void Dispose(bool disposing) { - if (!_isDisposed) + if (_isDisposed) return; + if (disposing) { - if (disposing) - { - _connectCancellationToken?.Dispose(); - _udp?.Dispose(); - _rtcpUdp?.Dispose(); - WebSocketClient?.Dispose(); - _connectionLock?.Dispose(); - } - - _isDisposed = true; + _connectCancellationToken?.Dispose(); + _udp?.Dispose(); + _rtcpUdp?.Dispose(); + WebSocketClient?.Dispose(); + _connectionLock?.Dispose(); } + + _isDisposed = true; } public void Dispose() => Dispose(true); - public async Task SendAsync(VoiceSocketFrameType type, uint sequence, object payload, RequestOptions options = null) + public async Task SendAsync(VoiceSocketFrameType type, + uint sequence, object payload, RequestOptions? options = null) { - object frame = new VoiceSocketRequestFrame { Type = type, Id = sequence, Request = true, Payload = payload }; + VoiceSocketRequestFrame frame = new() + { + Type = type, + Id = sequence, + Request = true, + Payload = payload + }; string json = SerializeJson(frame); - #if DEBUG_AUDIO Debug.WriteLine($""" - -> [#{sequence}] - [Payload] {json} - """); + -> [#{sequence}] + [Payload] {json} + """); #endif byte[] bytes = System.Text.Encoding.UTF8.GetBytes(json); _sequenceFrames[sequence] = type; @@ -238,46 +243,66 @@ private async Task SendRtcpAsync(byte[] data, int offset, int bytes) #region WebSocket - public async Task SendGetRouterRtpCapabilitiesRequestAsync(uint sequence, RequestOptions options = null) => + public async Task SendGetRouterRtpCapabilitiesRequestAsync(uint sequence, RequestOptions? options = null) => await SendAsync(VoiceSocketFrameType.GetRouterRtpCapabilities, sequence, new object(), options) .ConfigureAwait(false); - public async Task SendJoinRequestAsync(uint sequence, RequestOptions options = null) => - await SendAsync(VoiceSocketFrameType.Join, sequence, new JoinParams { DisplayName = string.Empty }, options).ConfigureAwait(false); + public async Task SendJoinRequestAsync(uint sequence, RequestOptions? options = null) + { + JoinParams args = new JoinParams { DisplayName = string.Empty }; + await SendAsync(VoiceSocketFrameType.Join, sequence, args, options).ConfigureAwait(false); + } - public async Task SendCreatePlainTransportRequestAsync(uint sequence, RequestOptions options = null) => - await SendAsync(VoiceSocketFrameType.CreatePlainTransport, sequence, - new CreatePlainTransportParams { Comedia = true, RtcpMultiplexing = false, Type = "plain" }, options).ConfigureAwait(false); + public async Task SendCreatePlainTransportRequestAsync(uint sequence, RequestOptions? options = null) + { + CreatePlainTransportParams args = new() + { + Comedia = true, + RtcpMultiplexing = false, + Type = "plain" + }; + await SendAsync(VoiceSocketFrameType.CreatePlainTransport, sequence, args, options).ConfigureAwait(false); + } public async Task SendProduceRequestAsync(uint sequence, ulong peerId, Guid transportId, uint ssrc, - RequestOptions options = null) => - await SendAsync(VoiceSocketFrameType.Produce, - sequence, - new ProduceParams + RequestOptions? options = null) + { + ProduceParams args = new() + { + AppData = new object(), + Kind = "audio", + PeerId = peerId.ToString(), + RtpParameters = new RtpParameters { - AppData = new object(), - Kind = "audio", - PeerId = peerId.ToString(), - RtpParameters = new RtpParameters - { - Codecs = - [ - new CodecParams + Codecs = + [ + new CodecParams + { + Channels = 2, + ClockRate = 48000, + MimeType = "audio/opus", + Parameters = new Parameters { - Channels = 2, - ClockRate = 48000, - MimeType = "audio/opus", - Parameters = new Parameters { SenderProduceStereo = 1 }, - PayloadType = 100 - } - ], - Encodings = [new EncodingParams { Ssrc = ssrc }] - }, - TransportId = transportId - }, options).ConfigureAwait(false); + SenderProduceStereo = 1 + }, + PayloadType = 100 + } + ], + Encodings = + [ + new EncodingParams + { + Ssrc = ssrc + } + ] + }, + TransportId = transportId + }; + await SendAsync(VoiceSocketFrameType.Produce, sequence, args, options).ConfigureAwait(false); + } public async Task SendRtcpAsync(uint ssrc, uint rtpTimestamp, uint sentPackets, uint sentOctets, - RequestOptions options = null) + RequestOptions? options = null) { byte[] packet = new byte[28]; // 10.. .... = Version: RFC 1889 Version (2) @@ -414,14 +439,9 @@ private async Task DisconnectInternalAsync() #region Helpers - private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - - private string SerializeJson(object payload) => - payload is null - ? string.Empty - : JsonSerializer.Serialize(payload, _serializerOptions); - - private T DeserializeJson(Stream jsonStream) => JsonSerializer.Deserialize(jsonStream, _serializerOptions); + [return: NotNullIfNotNull(nameof(payload))] + private string? SerializeJson(object? payload) => + payload is null ? null : JsonSerializer.Serialize(payload, _serializerOptions); #endregion } diff --git a/src/Kook.Net.WebSocket/Net/Converters/GatewaySocketFrameTypeConverter.cs b/src/Kook.Net.WebSocket/Net/Converters/GatewaySocketFrameTypeConverter.cs deleted file mode 100644 index eb5cbf7c..00000000 --- a/src/Kook.Net.WebSocket/Net/Converters/GatewaySocketFrameTypeConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Kook.API.Gateway; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Kook.Net.Converters; - -internal class GatewaySocketFrameTypeConverter : JsonConverter -{ - public override GatewaySocketFrameType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - (GatewaySocketFrameType)reader.GetInt32(); - - public override void Write(Utf8JsonWriter writer, GatewaySocketFrameType value, JsonSerializerOptions options) => - writer.WriteNumberValue((int)value); -} diff --git a/src/Kook.Net.WebSocket/Net/Converters/MessageTypeConverter.cs b/src/Kook.Net.WebSocket/Net/Converters/MessageTypeConverter.cs deleted file mode 100644 index 5de509a5..00000000 --- a/src/Kook.Net.WebSocket/Net/Converters/MessageTypeConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Kook.Net.Converters; - -internal class MessageTypeConverter : JsonConverter -{ - public override MessageType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => (MessageType)reader.GetInt32(); - - public override void Write(Utf8JsonWriter writer, MessageType value, JsonSerializerOptions options) => writer.WriteNumberValue((int)value); -} diff --git a/src/Kook.Net.WebSocket/Net/Converters/VoiceSocketFrameTypeConverter.cs b/src/Kook.Net.WebSocket/Net/Converters/VoiceSocketFrameTypeConverter.cs index 87ec7cfe..78ce6f12 100644 --- a/src/Kook.Net.WebSocket/Net/Converters/VoiceSocketFrameTypeConverter.cs +++ b/src/Kook.Net.WebSocket/Net/Converters/VoiceSocketFrameTypeConverter.cs @@ -15,11 +15,7 @@ public override void Write(Utf8JsonWriter writer, VoiceSocketFrameType value, Js { string method = value.ToString(); method = method.Length > 1 -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER ? method[..1].ToLower() + method[1..] -#else - ? method.Substring(0, 1).ToLower() + method.Substring(1) -#endif : method.ToLower(); writer.WriteStringValue(method); } diff --git a/src/Kook.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Kook.Net.WebSocket/Net/DefaultUdpSocket.cs index 7253bdd5..99d7959b 100644 --- a/src/Kook.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Kook.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -5,14 +5,15 @@ namespace Kook.Net.Udp; internal class DefaultUdpSocket : IUdpSocket, IDisposable { - public event Func ReceivedDatagram; + public event Func? ReceivedDatagram; private readonly SemaphoreSlim _lock; - private UdpClient _udp; - private IPEndPoint _destination; - private CancellationTokenSource _stopCancellationTokenSource, _cancellationTokenSource; + private UdpClient? _udp; + private IPEndPoint? _destination; + private CancellationTokenSource _stopCancellationTokenSource; + private CancellationTokenSource? _cancellationTokenSource; private CancellationToken _cancellationToken, _parentToken; - private Task _task; + private Task? _task; private bool _isDisposed; public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); @@ -44,7 +45,7 @@ private void Dispose(bool disposing) public async Task StartAsync() { - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await StartInternalAsync(_cancellationToken).ConfigureAwait(false); @@ -74,7 +75,7 @@ public async Task StartInternalAsync(CancellationToken cancellationToken) public async Task StopAsync() { - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await StopInternalAsync().ConfigureAwait(false); @@ -134,6 +135,8 @@ public async Task SendAsync(byte[] data, int index, int count) data = newData; } + if (_udp is null) + throw new InvalidOperationException("Socket is not started."); await _udp.SendAsync(data, count, _destination).ConfigureAwait(false); } @@ -142,6 +145,8 @@ private async Task RunAsync(CancellationToken cancellationToken) Task closeTask = Task.Delay(-1, cancellationToken); while (!cancellationToken.IsCancellationRequested) { + if (_udp is null) + throw new InvalidOperationException("Socket is not started."); Task receiveTask = _udp.ReceiveAsync(); _ = receiveTask.ContinueWith((receiveResult) => diff --git a/src/Kook.Net.WebSocket/Net/DefaultUdpSocketProvider.cs b/src/Kook.Net.WebSocket/Net/DefaultUdpSocketProvider.cs index 41963d1e..b5868885 100644 --- a/src/Kook.Net.WebSocket/Net/DefaultUdpSocketProvider.cs +++ b/src/Kook.Net.WebSocket/Net/DefaultUdpSocketProvider.cs @@ -16,7 +16,8 @@ public static class DefaultUdpSocketProvider } catch (PlatformNotSupportedException ex) { - throw new PlatformNotSupportedException("The default UdpSocketProvider is not supported on this platform.", ex); + throw new PlatformNotSupportedException( + "The default UdpSocketProvider is not supported on this platform.", ex); } }; } diff --git a/src/Kook.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Kook.Net.WebSocket/Net/DefaultWebSocketClient.cs index f4b9507a..9405b414 100644 --- a/src/Kook.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Kook.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -11,21 +11,23 @@ internal class DefaultWebSocketClient : IWebSocketClient, IDisposable public const int SendChunkSize = 4 * 1024; //4KB private const int HR_TIMEOUT = -2147012894; - public event Func BinaryMessage; - public event Func TextMessage; - public event Func Closed; + public event Func? BinaryMessage; + public event Func? TextMessage; + public event Func? Closed; private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; - private readonly IWebProxy _proxy; + private readonly IWebProxy? _proxy; private TimeSpan _keepAliveInterval; - private ClientWebSocket _client; - private Task _task; - private CancellationTokenSource _disconnectTokenSource, _cancellationTokenSource; - private CancellationToken _cancellationToken, _parentToken; + private ClientWebSocket? _client; + private Task? _task; + private CancellationTokenSource _disconnectTokenSource; + private CancellationTokenSource? _cancellationTokenSource; + private CancellationToken _cancellationToken; + private CancellationToken _parentToken; private bool _isDisposed, _isDisconnecting; - public DefaultWebSocketClient(IWebProxy proxy = null) + public DefaultWebSocketClient(IWebProxy? proxy = null) { _lock = new SemaphoreSlim(1, 1); _disconnectTokenSource = new CancellationTokenSource(); @@ -38,25 +40,22 @@ public DefaultWebSocketClient(IWebProxy proxy = null) private void Dispose(bool disposing) { - if (!_isDisposed) + if (_isDisposed) return; + if (disposing) { - if (disposing) - { - DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); - _disconnectTokenSource?.Dispose(); - _cancellationTokenSource?.Dispose(); - _lock?.Dispose(); - } - - _isDisposed = true; + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); + _disconnectTokenSource?.Dispose(); + _cancellationTokenSource?.Dispose(); + _lock?.Dispose(); } + _isDisposed = true; } public void Dispose() => Dispose(true); public async Task ConnectAsync(string host) { - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await ConnectInternalAsync(host).ConfigureAwait(false); @@ -83,8 +82,10 @@ private async Task ConnectInternalAsync(string host) _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = _keepAliveInterval; foreach (KeyValuePair header in _headers) + { if (header.Value != null) _client.Options.SetRequestHeader(header.Key, header.Value); + } await _client.ConnectAsync(new Uri(host), _cancellationToken).ConfigureAwait(false); _task = RunAsync(_cancellationToken); @@ -92,7 +93,7 @@ private async Task ConnectInternalAsync(string host) public async Task DisconnectAsync(int closeCode = 1000) { - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await DisconnectInternalAsync(closeCode).ConfigureAwait(false); @@ -106,7 +107,6 @@ public async Task DisconnectAsync(int closeCode = 1000) private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { _isDisconnecting = true; - if (_client != null) { if (!isDisposing) @@ -145,8 +145,11 @@ private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposin try { - await (_task ?? Task.Delay(0)).ConfigureAwait(false); - _task = null; + if (_task is not null) + { + await _task.ConfigureAwait(false); + _task = null; + } } finally { @@ -156,9 +159,10 @@ private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposin private async Task OnClosed(Exception ex) { - if (_isDisconnecting) return; //Ignore, this disconnect was requested. + if (_isDisconnecting) + return; //Ignore, this disconnect was requested. - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await DisconnectInternalAsync(isDisposing: false); @@ -168,7 +172,8 @@ private async Task OnClosed(Exception ex) _lock.Release(); } - await Closed(ex); + if (Closed is not null) + await Closed(ex); } public void SetHeader(string key, string value) => _headers[key] = value; @@ -207,15 +212,10 @@ public async Task SendAsync(byte[] data, int index, int count, bool isText) for (int i = 0; i < frameCount; i++, index += SendChunkSize) { bool isLast = i == frameCount - 1; - - int frameSize; - if (isLast) - frameSize = count - i * SendChunkSize; - else - frameSize = SendChunkSize; - + int frameSize = isLast ? count - i * SendChunkSize : SendChunkSize; WebSocketMessageType type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; - await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancellationToken).ConfigureAwait(false); + ArraySegment arraySegment = new(data, index, count); + await _client.SendAsync(arraySegment, type, isLast, _cancellationToken).ConfigureAwait(false); } } finally @@ -232,31 +232,36 @@ private async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { + if (_client is null) + throw new InvalidOperationException("The client is not created."); WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - byte[] result; + byte[]? result; int resultCount; if (socketResult.MessageType == WebSocketMessageType.Close) - throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); + throw new WebSocketClosedException((int?)socketResult.CloseStatus, socketResult.CloseStatusDescription); if (!socketResult.EndOfMessage) + { //This is a large message (likely just READY), lets create a temporary expandable stream - using (MemoryStream stream = new()) - { + using MemoryStream stream = new(); + if (buffer.Array is not null) stream.Write(buffer.Array, 0, socketResult.Count); - do - { - if (cancellationToken.IsCancellationRequested) return; - - socketResult = await _client.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); + do + { + if (cancellationToken.IsCancellationRequested) return; + socketResult = await _client.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); + if (buffer.Array is not null) stream.Write(buffer.Array, 0, socketResult.Count); - } while (socketResult == null || !socketResult.EndOfMessage); + } while (socketResult is not { EndOfMessage: true }); - //Use the internal buffer if we can get it - resultCount = (int)stream.Length; + //Use the internal buffer if we can get it + resultCount = (int)stream.Length; - result = stream.TryGetBuffer(out ArraySegment streamBuffer) ? streamBuffer.Array : stream.ToArray(); - } + result = stream.TryGetBuffer(out ArraySegment streamBuffer) + ? streamBuffer.Array + : stream.ToArray(); + } else { //Small message @@ -264,18 +269,21 @@ private async Task RunAsync(CancellationToken cancellationToken) result = buffer.Array; } - if (socketResult.MessageType == WebSocketMessageType.Text) + if (result is not null) { - string text = Encoding.UTF8.GetString(result, 0, resultCount); - await TextMessage(text).ConfigureAwait(false); + if (socketResult.MessageType == WebSocketMessageType.Text && TextMessage is not null) + { + string text = Encoding.UTF8.GetString(result, 0, resultCount); + if (TextMessage is not null) + await TextMessage(text).ConfigureAwait(false); + } + else if (BinaryMessage is not null) await BinaryMessage(result, 0, resultCount).ConfigureAwait(false); } - else - await BinaryMessage(result, 0, resultCount).ConfigureAwait(false); } } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { - Task _ = OnClosed(new Exception("Connection timed out.", ex)); + _ = OnClosed(new Exception("Connection timed out.", ex)); } catch (OperationCanceledException) { @@ -284,7 +292,7 @@ private async Task RunAsync(CancellationToken cancellationToken) catch (Exception ex) { //This cannot be awaited otherwise we'll deadlock when KookApiClient waits for this task to complete. - Task _ = OnClosed(ex); + _ = OnClosed(ex); } } } diff --git a/src/Kook.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Kook.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 1b47526d..982099ec 100644 --- a/src/Kook.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Kook.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -18,7 +18,7 @@ public static class DefaultWebSocketProvider /// The proxy to use. /// A delegate that creates a new instance. /// The default WebSocketProvider is not supported on this platform. - public static WebSocketProvider Create(IWebProxy proxy = null) => + public static WebSocketProvider Create(IWebProxy? proxy = null) => () => { try diff --git a/test/Kook.Net.Tests.Integration/ChannelTests.cs b/test/Kook.Net.Tests.Integration/ChannelTests.cs index 457c965e..ed6cd0be 100644 --- a/test/Kook.Net.Tests.Integration/ChannelTests.cs +++ b/test/Kook.Net.Tests.Integration/ChannelTests.cs @@ -103,7 +103,7 @@ await channel.ModifyAsync(x => public async Task ModifyChannelCategories() { // util method for checking if a category is set - async Task CheckAsync(INestedChannel channel, ICategoryChannel cat) + async Task CheckAsync(INestedChannel channel, ICategoryChannel? cat) { // check that the category is not set if (cat == null) @@ -115,7 +115,7 @@ async Task CheckAsync(INestedChannel channel, ICategoryChannel cat) { Assert.NotNull(channel.CategoryId); Assert.Equal(cat.Id, channel.CategoryId); - ICategoryChannel getCat = await channel.GetCategoryAsync(); + ICategoryChannel? getCat = await channel.GetCategoryAsync(); Assert.NotNull(getCat); Assert.Equal(cat.Id, getCat.Id); } @@ -168,11 +168,11 @@ async Task CheckAsync(INestedChannel channel, ICategoryChannel cat) public async Task MiscAsync() { ICategoryChannel category = await _guild.CreateCategoryChannelAsync("CATEGORY"); - RestTextChannel channel = await _guild.CreateTextChannelAsync("TEXT", + RestTextChannel? channel = await _guild.CreateTextChannelAsync("TEXT", p => p.CategoryId = category.Id) as RestTextChannel; try { - IGuildUser selfUser = await _guild.GetCurrentUserAsync(); + IGuildUser? selfUser = await _guild.GetCurrentUserAsync(); IRole role = await _guild.CreateRoleAsync("TEST ROLE"); Assert.NotNull(category); Assert.NotNull(channel); @@ -230,7 +230,8 @@ await channel.ModifyPermissionOverwriteAsync(role, permissions => permissions } finally { - await channel.DeleteAsync(); + if (channel is not null) + await channel.DeleteAsync(); await category.DeleteAsync(); } } diff --git a/test/Kook.Net.Tests.Integration/Fixtures/KookRestClientFixture.cs b/test/Kook.Net.Tests.Integration/Fixtures/KookRestClientFixture.cs index 0fdd9c30..39d0d8c4 100644 --- a/test/Kook.Net.Tests.Integration/Fixtures/KookRestClientFixture.cs +++ b/test/Kook.Net.Tests.Integration/Fixtures/KookRestClientFixture.cs @@ -14,20 +14,26 @@ public class KookRestClientFixture : IDisposable, IAsyncDisposable public KookRestClientFixture() { - string token = Environment.GetEnvironmentVariable("KOOK_NET_TEST_TOKEN"); - if (string.IsNullOrWhiteSpace(token)) throw new Exception("The KOOK_NET_TEST_TOKEN environment variable was not provided."); + string? token = Environment.GetEnvironmentVariable("KOOK_NET_TEST_TOKEN"); + if (string.IsNullOrWhiteSpace(token)) + throw new Exception("The KOOK_NET_TEST_TOKEN environment variable was not provided."); - Client = new KookRestClient(new KookRestConfig() { LogLevel = LogSeverity.Debug, DefaultRetryMode = RetryMode.AlwaysRetry }); + Client = new KookRestClient(new KookRestConfig + { + LogLevel = LogSeverity.Debug, + DefaultRetryMode = RetryMode.AlwaysRetry + }); Client.LoginAsync(TokenType.Bot, token).GetAwaiter().GetResult(); } /// public virtual async ValueTask DisposeAsync() { + GC.SuppressFinalize(this); await Client.LogoutAsync(); Client.Dispose(); } /// - public virtual void Dispose() => DisposeAsync().GetAwaiter().GetResult(); + public virtual void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); } diff --git a/test/Kook.Net.Tests.Integration/Fixtures/RestChannelFixture.cs b/test/Kook.Net.Tests.Integration/Fixtures/RestChannelFixture.cs index 80149ef2..b160ec88 100644 --- a/test/Kook.Net.Tests.Integration/Fixtures/RestChannelFixture.cs +++ b/test/Kook.Net.Tests.Integration/Fixtures/RestChannelFixture.cs @@ -31,5 +31,5 @@ public override async ValueTask DisposeAsync() } /// - public override void Dispose() => DisposeAsync().GetAwaiter().GetResult(); + public override void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); } diff --git a/test/Kook.Net.Tests.Integration/Fixtures/RestGuildFixture.cs b/test/Kook.Net.Tests.Integration/Fixtures/RestGuildFixture.cs index 8881c953..96de64fe 100644 --- a/test/Kook.Net.Tests.Integration/Fixtures/RestGuildFixture.cs +++ b/test/Kook.Net.Tests.Integration/Fixtures/RestGuildFixture.cs @@ -16,7 +16,7 @@ public RestGuildFixture() : base() { const string guildName = "KOOK NET INTEGRATION TEST"; List guilds = Client.GetGuildsAsync().GetAwaiter().GetResult() - .Where(x => x.OwnerId == Client.CurrentUser.Id) + .Where(x => x.OwnerId == Client.CurrentUser?.Id) .Where(x => x.Name == guildName) .ToList(); RemoveUselessTestGuilds(guilds.Skip(1)); @@ -59,5 +59,5 @@ public override async ValueTask DisposeAsync() } /// - public override void Dispose() => DisposeAsync().GetAwaiter().GetResult(); + public override void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); } diff --git a/test/Kook.Net.Tests.Integration/GuildTests.cs b/test/Kook.Net.Tests.Integration/GuildTests.cs index 32202ba2..48aab9bd 100644 --- a/test/Kook.Net.Tests.Integration/GuildTests.cs +++ b/test/Kook.Net.Tests.Integration/GuildTests.cs @@ -35,7 +35,8 @@ private Task LogAsync(LogMessage message) [Fact] public async Task GetUsersAsync() { - IGuildUser currentUser = await _guild.GetCurrentUserAsync(); + IGuildUser? currentUser = await _guild.GetCurrentUserAsync(); + Assert.NotNull(currentUser); IReadOnlyCollection users = await _guild.GetUsersAsync(); users.Should().ContainSingle(x => x.Id == currentUser.Id); @@ -50,25 +51,24 @@ public async Task GetUsersAsync() .Excluding(x => x.IsDenoiseEnabled) .Excluding(x => x.IsOwner) .Excluding(x => x.IsSystemUser) - // Due to different domain names - .Excluding(x => x.Avatar) - .Excluding(x => x.BuffAvatar) + .Excluding(x => x.Color) ); } [Fact] public async Task ModifyNicknameAsync() { - IGuildUser currentUser = await _guild.GetCurrentUserAsync(); + IGuildUser? currentUser = await _guild.GetCurrentUserAsync(); + Assert.NotNull(currentUser); await currentUser.ModifyNicknameAsync("UPDATED NICKNAME"); currentUser.Nickname.Should().Be("UPDATED NICKNAME"); - (await _guild.GetCurrentUserAsync()).Nickname.Should().Be("UPDATED NICKNAME"); + (await _guild.GetCurrentUserAsync())?.Nickname.Should().Be("UPDATED NICKNAME"); await currentUser.ModifyNicknameAsync(null); currentUser.Nickname.Should().BeNullOrEmpty(); - (await _guild.GetCurrentUserAsync()).Nickname.Should().BeNullOrEmpty(); + (await _guild.GetCurrentUserAsync())?.Nickname.Should().BeNullOrEmpty(); currentUser.DisplayName.Should().Be(currentUser.Username); - (await _guild.GetCurrentUserAsync()).DisplayName.Should().Be(currentUser.Username); + (await _guild.GetCurrentUserAsync())?.DisplayName.Should().Be(currentUser.Username); } } diff --git a/test/Kook.Net.Tests.Integration/Kook.Net.Tests.Integration.csproj b/test/Kook.Net.Tests.Integration/Kook.Net.Tests.Integration.csproj index 4cb4bec7..136826e1 100644 --- a/test/Kook.Net.Tests.Integration/Kook.Net.Tests.Integration.csproj +++ b/test/Kook.Net.Tests.Integration/Kook.Net.Tests.Integration.csproj @@ -1,36 +1,5 @@ - - net8.0 - disable - false - Kook.Net.Tests - default - NU1803 - + - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - diff --git a/test/Kook.Net.Tests.Integration/KookRestApiClientTests.cs b/test/Kook.Net.Tests.Integration/KookRestApiClientTests.cs index 03aaaec3..770e3e56 100644 --- a/test/Kook.Net.Tests.Integration/KookRestApiClientTests.cs +++ b/test/Kook.Net.Tests.Integration/KookRestApiClientTests.cs @@ -15,24 +15,23 @@ public class KookRestApiClientTests : IClassFixture, IAsyncDis { private readonly KookRestApiClient _apiClient; - public KookRestApiClientTests(RestGuildFixture guildFixture) => _apiClient = guildFixture.Client.ApiClient; - - public ValueTask DisposeAsync() + public KookRestApiClientTests(RestGuildFixture guildFixture) { -#if NET5_0_OR_GREATER - return ValueTask.CompletedTask; -#else - return new ValueTask(Task.CompletedTask); -#endif + _apiClient = guildFixture.Client.ApiClient; } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + [Fact] public async Task CreateAsset_WithMaximumSize_DontThrowsException() { ulong fileSize = (ulong)(30 * Math.Pow(2, 20)); using MemoryStream stream = new(new byte[fileSize]); CreateAssetResponse response = - await _apiClient.CreateAssetAsync(new CreateAssetParams { File = stream, FileName = "test.file" }); + await _apiClient.CreateAssetAsync(new CreateAssetParams + { + File = stream, FileName = "test.file" + }); response.Url.Should().NotBeNullOrWhiteSpace(); } @@ -43,7 +42,10 @@ public async Task CreateAsset_WithOverSize_ThrowsException() { ulong fileSize = (ulong)(30 * Math.Pow(2, 20)) + 1; using MemoryStream stream = new(new byte[fileSize]); - await _apiClient.CreateAssetAsync(new CreateAssetParams { File = stream, FileName = "test.file" }); + await _apiClient.CreateAssetAsync(new CreateAssetParams + { + File = stream, FileName = "test.file" + }); }; await upload.Should().ThrowExactlyAsync() diff --git a/test/Kook.Net.Tests.Integration/MessageInTextTests.cs b/test/Kook.Net.Tests.Integration/MessageInTextTests.cs index a14d0620..f7b21c70 100644 --- a/test/Kook.Net.Tests.Integration/MessageInTextTests.cs +++ b/test/Kook.Net.Tests.Integration/MessageInTextTests.cs @@ -17,14 +17,12 @@ public class MessageInTextTests : IClassFixture { private readonly IGuild _guild; private readonly ITextChannel _channel; - private readonly IGuildUser _selfUser; private readonly ITestOutputHelper _output; public MessageInTextTests(RestChannelFixture channelFixture, ITestOutputHelper output) { _guild = channelFixture.Guild; _channel = channelFixture.TextChannel; - _selfUser = _guild.GetCurrentUserAsync().GetAwaiter().GetResult(); _output = output; _output.WriteLine($"RestGuildFixture using guild: {_guild.Id}"); // capture all console output @@ -40,13 +38,15 @@ private Task LogAsync(LogMessage message) [Fact] public async Task SendTextAsync() { + IGuildUser? selfUser = await _guild.GetCurrentUserAsync(); + Assert.NotNull(selfUser); string kMarkdownSourceContent = @$"*TEST* **KMARKDOWN** ~~MESSAGE~~ > NOTHING [INLINE LINK](https://kooknet.dev) (ins)UNDERLINE(ins)(spl)SPOLIER(spl) :maple_leaf: {_channel.KMarkdownMention} -{_selfUser.KMarkdownMention} +{selfUser.KMarkdownMention} {_guild.EveryoneRole.KMarkdownMention} (met)here(met) `INLINE CODE` @@ -60,7 +60,7 @@ [INLINE LINK](https://kooknet.dev) (ins)UNDERLINE(ins)(spl)SPOLIER(spl) 🍁 {_channel.KMarkdownMention} -{_selfUser.KMarkdownMention} +{selfUser.KMarkdownMention} {_guild.EveryoneRole.KMarkdownMention} (met)here(met) `INLINE CODE` @@ -83,10 +83,10 @@ INLINE CODE CODE BLOCK "; Cacheable cacheable = await _channel.SendTextAsync(kMarkdownSourceContent); - IUserMessage message = await cacheable.GetOrDownloadAsync(); + IUserMessage? message = await cacheable.GetOrDownloadAsync(); try { - IGuildUser selfUser = await _guild.GetCurrentUserAsync(); + selfUser = await _guild.GetCurrentUserAsync(); Assert.NotNull(selfUser); Assert.NotEqual(Guid.Empty, cacheable.Id); Assert.False(cacheable.HasValue); @@ -99,22 +99,23 @@ CODE BLOCK Assert.Equal(4, message.Tags.Count); List tags = message.Tags.ToList(); Assert.Equal(_channel.Id, tags.Single(tag => tag.Type == TagType.ChannelMention).Key); - Assert.Equal(_selfUser.Id, tags.Single(tag => tag.Type == TagType.UserMention).Key); + Assert.Equal(selfUser.Id, tags.Single(tag => tag.Type == TagType.UserMention).Key); Assert.Equal(0, tags.Single(tag => tag.Type == TagType.EveryoneMention).Key); Assert.Equal(0, tags.Single(tag => tag.Type == TagType.HereMention).Key); } finally { - await message.DeleteAsync(); + if (message != null) + await message.DeleteAsync(); } } [Fact] public async Task ReactionsAsync() { - IGuildUser currentUser = await _guild.GetCurrentUserAsync(); + IGuildUser? currentUser = await _guild.GetCurrentUserAsync(); Cacheable cacheable = await _channel.SendTextAsync("TEST MESSAGE"); - RestMessage message = await cacheable.GetOrDownloadAsync() as RestMessage; + RestMessage? message = await cacheable.GetOrDownloadAsync() as RestMessage; try { Assert.NotNull(message); @@ -139,7 +140,8 @@ public async Task ReactionsAsync() } finally { - await message.DeleteAsync(); + if (message != null) + await message.DeleteAsync(); } } } diff --git a/test/Kook.Net.Tests.Integration/MessageInVoiceTests.cs b/test/Kook.Net.Tests.Integration/MessageInVoiceTests.cs index f59ef391..058a6622 100644 --- a/test/Kook.Net.Tests.Integration/MessageInVoiceTests.cs +++ b/test/Kook.Net.Tests.Integration/MessageInVoiceTests.cs @@ -17,14 +17,12 @@ public class MessageInVoiceTests : IClassFixture { private readonly IGuild _guild; private readonly IVoiceChannel _channel; - private readonly IGuildUser _selfUser; private readonly ITestOutputHelper _output; public MessageInVoiceTests(RestChannelFixture channelFixture, ITestOutputHelper output) { _guild = channelFixture.Guild; _channel = channelFixture.VoiceChannel; - _selfUser = _guild.GetCurrentUserAsync().GetAwaiter().GetResult(); _output = output; _output.WriteLine($"RestGuildFixture using guild: {_guild.Id}"); // capture all console output @@ -40,13 +38,15 @@ private Task LogAsync(LogMessage message) [Fact] public async Task SendTextAsync() { + IGuildUser? selfUser = await _guild.GetCurrentUserAsync(); + Assert.NotNull(selfUser); string kMarkdownSourceContent = @$"*TEST* **KMARKDOWN** ~~MESSAGE~~ > NOTHING [INLINE LINK](https://kooknet.dev) (ins)UNDERLINE(ins)(spl)SPOLIER(spl) :maple_leaf: {_channel.KMarkdownMention} -{_selfUser.KMarkdownMention} +{selfUser.KMarkdownMention} {_guild.EveryoneRole.KMarkdownMention} (met)here(met) `INLINE CODE` @@ -60,7 +60,7 @@ [INLINE LINK](https://kooknet.dev) (ins)UNDERLINE(ins)(spl)SPOLIER(spl) 🍁 {_channel.KMarkdownMention} -{_selfUser.KMarkdownMention} +{selfUser.KMarkdownMention} {_guild.EveryoneRole.KMarkdownMention} (met)here(met) `INLINE CODE` @@ -83,10 +83,10 @@ INLINE CODE CODE BLOCK "; Cacheable cacheable = await _channel.SendTextAsync(kMarkdownSourceContent); - IUserMessage message = await cacheable.GetOrDownloadAsync(); + IUserMessage? message = await cacheable.GetOrDownloadAsync(); try { - IGuildUser selfUser = await _guild.GetCurrentUserAsync(); + selfUser = await _guild.GetCurrentUserAsync(); Assert.NotNull(selfUser); Assert.NotEqual(Guid.Empty, cacheable.Id); Assert.False(cacheable.HasValue); @@ -99,22 +99,23 @@ CODE BLOCK Assert.Equal(4, message.Tags.Count); List tags = message.Tags.ToList(); Assert.Equal(_channel.Id, tags.Single(tag => tag.Type == TagType.ChannelMention).Key); - Assert.Equal(_selfUser.Id, tags.Single(tag => tag.Type == TagType.UserMention).Key); + Assert.Equal(selfUser.Id, tags.Single(tag => tag.Type == TagType.UserMention).Key); Assert.Equal(0, tags.Single(tag => tag.Type == TagType.EveryoneMention).Key); Assert.Equal(0, tags.Single(tag => tag.Type == TagType.HereMention).Key); } finally { - await message.DeleteAsync(); + if (message != null) + await message.DeleteAsync(); } } [Fact] public async Task ReactionsAsync() { - IGuildUser currentUser = await _guild.GetCurrentUserAsync(); + IGuildUser? currentUser = await _guild.GetCurrentUserAsync(); Cacheable cacheable = await _channel.SendTextAsync("TEST MESSAGE"); - RestMessage message = await cacheable.GetOrDownloadAsync() as RestMessage; + RestMessage? message = await cacheable.GetOrDownloadAsync() as RestMessage; try { Assert.NotNull(message); @@ -139,7 +140,8 @@ public async Task ReactionsAsync() } finally { - await message.DeleteAsync(); + if (message != null) + await message.DeleteAsync(); } } } diff --git a/test/Kook.Net.Tests.Integration/RoleTests.cs b/test/Kook.Net.Tests.Integration/RoleTests.cs index 6997d31c..08851e22 100644 --- a/test/Kook.Net.Tests.Integration/RoleTests.cs +++ b/test/Kook.Net.Tests.Integration/RoleTests.cs @@ -67,7 +67,7 @@ public async Task AddRemoveRoleAsync() IRole role = await _guild.CreateRoleAsync("TEST ROLE"); try { - RestGuildUser selfUser = await _guild.GetCurrentUserAsync() as RestGuildUser; + RestGuildUser? selfUser = await _guild.GetCurrentUserAsync() as RestGuildUser; Assert.NotNull(role); Assert.NotNull(selfUser); // check that the bot can add the role diff --git a/test/Kook.Net.Tests.Unit/CardBuilderTests.cs b/test/Kook.Net.Tests.Unit/CardBuilderTests.cs index 69125132..54dd8f54 100644 --- a/test/Kook.Net.Tests.Unit/CardBuilderTests.cs +++ b/test/Kook.Net.Tests.Unit/CardBuilderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Xunit; namespace Kook; @@ -30,14 +29,14 @@ public void Constructor() Assert.Equal(CardSize.Small, builder.Build().Size); Assert.Equal(Color.Blue, builder.Color); Assert.Equal(Color.Blue, builder.Build().Color); - Assert.Equal("text", ((KMarkdownElementBuilder)((SectionModuleBuilder)builder.Modules[0]).Text).Content); - Assert.Equal("text", ((KMarkdownElement)((SectionModule)builder.Build().Modules[0]).Text).Content); + Assert.Equal("text", ((KMarkdownElementBuilder?)((SectionModuleBuilder)builder.Modules[0]).Text)?.Content); + Assert.Equal("text", ((KMarkdownElement?)((SectionModule)builder.Build().Modules[0]).Text)?.Content); builder = new CardBuilder { Theme = CardTheme.Danger, Size = CardSize.Large, Color = Color.Red, - Modules = new List { new SectionModuleBuilder { Text = new PlainTextElementBuilder { Content = "content" } } } + Modules = [new SectionModuleBuilder { Text = new PlainTextElementBuilder { Content = "content" } }] }; Assert.Equal(CardTheme.Danger, builder.Theme); Assert.Equal(CardTheme.Danger, builder.Build().Theme); @@ -45,8 +44,8 @@ public void Constructor() Assert.Equal(CardSize.Large, builder.Build().Size); Assert.Equal(Color.Red, builder.Color); Assert.Equal(Color.Red, builder.Build().Color); - Assert.Equal("content", ((PlainTextElementBuilder)((SectionModuleBuilder)builder.Modules[0]).Text).Content); - Assert.Equal("content", ((PlainTextElement)((SectionModule)builder.Build().Modules[0]).Text).Content); + Assert.Equal("content", ((PlainTextElementBuilder?)((SectionModuleBuilder)builder.Modules[0]).Text)?.Content); + Assert.Equal("content", ((PlainTextElement?)((SectionModule)builder.Build().Modules[0]).Text)?.Content); } /// @@ -64,14 +63,14 @@ public void WithTheme() } /// - /// Tests the behavior of . + /// Tests the behavior of . /// [Fact] public void WithColor() { CardBuilder builder = new CardBuilder().WithColor(Color.Red); - Assert.Equal(Color.Red.RawValue, builder.Color.Value.RawValue); - Assert.Equal(Color.Red.RawValue, builder.Build().Color.Value.RawValue); + Assert.Equal(Color.Red.RawValue, builder.Color?.RawValue); + Assert.Equal(Color.Red.RawValue, builder.Build().Color?.RawValue); } /// diff --git a/test/Kook.Net.Tests.Unit/CardXmlTests.cs b/test/Kook.Net.Tests.Unit/CardXmlTests.cs index 20c07705..0f2521b9 100644 --- a/test/Kook.Net.Tests.Unit/CardXmlTests.cs +++ b/test/Kook.Net.Tests.Unit/CardXmlTests.cs @@ -110,24 +110,24 @@ public void XmlParse() Assert.Equal(13, full.Modules.Length); Assert.IsType(full.Modules[0]); - Assert.Equal("SECTION_HEADER", ((HeaderModule)full.Modules[0]).Text.Content); + Assert.Equal("SECTION_HEADER", ((HeaderModule?)full.Modules[0])?.Text?.Content); Assert.IsType(full.Modules[1]); Assert.Equal(SectionAccessoryMode.Right, ((SectionModule)full.Modules[1])?.Mode); Assert.False((((SectionModule)full.Modules[1]).Text as PlainTextElement)?.Emoji); Assert.Equal("SECTION_PLAIN", (((SectionModule)full.Modules[1]).Text as PlainTextElement)?.Content); Assert.Equal(ButtonTheme.Secondary, (((SectionModule)full.Modules[1]).Accessory as ButtonElement)?.Theme); - Assert.Equal(ButtonClickEventType.ReturnValue, ((ButtonElement)((SectionModule)full.Modules[1]).Accessory).Click); - Assert.Equal("SECTION_ACCESSORY_BUTTON_VALUE", ((ButtonElement)((SectionModule)full.Modules[1]).Accessory).Value); - Assert.Equal("SECTION_ACCESSORY_BUTTON", (((ButtonElement)((SectionModule)full.Modules[1]).Accessory)?.Text as PlainTextElement)?.Content); + Assert.Equal(ButtonClickEventType.ReturnValue, ((ButtonElement?)((SectionModule)full.Modules[1])?.Accessory)?.Click); + Assert.Equal("SECTION_ACCESSORY_BUTTON_VALUE", ((ButtonElement?)((SectionModule)full.Modules[1])?.Accessory)?.Value); + Assert.Equal("SECTION_ACCESSORY_BUTTON", (((ButtonElement?)((SectionModule)full.Modules[1])?.Accessory)?.Text as PlainTextElement)?.Content); Assert.IsType(full.Modules[2]); - Assert.Equal(SectionAccessoryMode.Unspecified, ((SectionModule)full.Modules[2])?.Mode); + Assert.Null(((SectionModule)full.Modules[2])?.Mode); Assert.Equal("SECTION_KMARKDOWN", (((SectionModule)full.Modules[2]).Text as KMarkdownElement)?.Content); Assert.Equal("https://SECTION_MOCK/IMAGE.jpg", (((SectionModule)full.Modules[2]).Accessory as ImageElement)?.Source); - Assert.Equal(ImageSize.Small, ((ImageElement)((SectionModule)full.Modules[2]).Accessory).Size); - Assert.Equal("IMAGE_ALT", ((ImageElement)((SectionModule)full.Modules[2]).Accessory).Alternative); - Assert.True(((ImageElement)((SectionModule)full.Modules[2]).Accessory).Circle); + Assert.Equal(ImageSize.Small, ((ImageElement?)((SectionModule)full.Modules[2]).Accessory)?.Size); + Assert.Equal("IMAGE_ALT", ((ImageElement?)((SectionModule)full.Modules[2]).Accessory)?.Alternative); + Assert.True(((ImageElement?)((SectionModule)full.Modules[2]).Accessory)?.Circle); Assert.IsType(full.Modules[3]); Assert.Equal(3, ((ImageGroupModule)full.Modules[3])?.Elements.Length); diff --git a/test/Kook.Net.Tests.Unit/ChannelPermissionsTests.cs b/test/Kook.Net.Tests.Unit/ChannelPermissionsTests.cs index 6b0cf5c3..9aa060e9 100644 --- a/test/Kook.Net.Tests.Unit/ChannelPermissionsTests.cs +++ b/test/Kook.Net.Tests.Unit/ChannelPermissionsTests.cs @@ -39,8 +39,6 @@ IEnumerable GetTestValues() yield return ChannelPermissions.Voice.RawValue; } - ; - foreach (ulong rawValue in GetTestValues()) { ChannelPermissions p = new(rawValue); diff --git a/test/Kook.Net.Tests.Unit/ElementBuilderTests.cs b/test/Kook.Net.Tests.Unit/ElementBuilderTests.cs index 83d4bf57..88c7e239 100644 --- a/test/Kook.Net.Tests.Unit/ElementBuilderTests.cs +++ b/test/Kook.Net.Tests.Unit/ElementBuilderTests.cs @@ -8,8 +8,7 @@ namespace Kook; public class ElementBuilderTests { private const string Name = "Kook.Net"; - private const string Icon = "https://kaiheila.net/logo.png"; - private const string Url = "https://kaiheila.net/"; + private const string Icon = "https://www.kookapp.cn/logo.png"; [Fact] public void PlainTextElementBuilder_Constructor() @@ -51,13 +50,22 @@ IEnumerable GetValidContent() [Fact] public void PlainTextElementBuilder_InvalidContent() { - IEnumerable GetInvalidContent() + IEnumerable GetInvalidContent() { yield return null; yield return new string('a', 2001); } - foreach (string content in GetInvalidContent()) Assert.Throws(() => new PlainTextElementBuilder().WithContent(content)); + foreach (string? content in GetInvalidContent()) + { + Assert.ThrowsAny(() => + { + new PlainTextElementBuilder + { + Content = content + }.Build(); + }); + } } [Theory] @@ -107,13 +115,22 @@ IEnumerable GetValidContent() [Fact] public void KMarkdownElementBuilder_InvalidContent() { - IEnumerable GetInvalidContent() + IEnumerable GetInvalidContent() { yield return null; yield return new string('a', 5001); } - foreach (string content in GetInvalidContent()) Assert.Throws(() => new KMarkdownElementBuilder().WithContent(content)); + foreach (string? content in GetInvalidContent()) + { + Assert.ThrowsAny(() => + { + new KMarkdownElementBuilder + { + Content = content + }.Build(); + }); + } } [Theory] @@ -143,7 +160,6 @@ public void ImageElementBuilder_Constructor() Assert.Equal(Name, builder.Build().Alternative); Assert.Equal(ImageSize.Small, builder.Size); Assert.Equal(ImageSize.Small, builder.Build().Size); - ; Assert.Equal(true, builder.Circle); Assert.Equal(true, builder.Build().Circle); builder = new ImageElementBuilder { Source = Icon, Alternative = Name, Size = ImageSize.Large, Circle = false }; @@ -155,7 +171,6 @@ public void ImageElementBuilder_Constructor() Assert.Equal(Name, builder.Build().Alternative); Assert.Equal(ImageSize.Large, builder.Size); Assert.Equal(ImageSize.Large, builder.Build().Size); - ; Assert.Equal(false, builder.Circle); Assert.Equal(false, builder.Build().Circle); } @@ -167,9 +182,9 @@ public void ImageElementBuilder_Constructor() [InlineData("alternative")] // 20 characters [InlineData("abcdefghijklmnopqrst")] - public void ImageElementBuilder_ValidAlternative(string alternative) + public void ImageElementBuilder_ValidAlternative(string? alternative) { - ImageElementBuilder builder = new ImageElementBuilder().WithAlternative(alternative); + ImageElementBuilder builder = new ImageElementBuilder().WithSource("https://www.kookapp.cn/").WithAlternative(alternative); Assert.Equal(alternative, builder.Alternative); Assert.Equal(alternative, builder.Build().Alternative); } @@ -178,24 +193,22 @@ public void ImageElementBuilder_ValidAlternative(string alternative) // 21 characters [InlineData("abcdefghijklmnopqrstu")] public void ImageElementBuilder_InvalidAlternative(string alternative) => - Assert.Throws(() => new ImageElementBuilder().WithAlternative(alternative)); + Assert.Throws(() => new ImageElementBuilder().WithSource("https://www.kookapp.cn/").WithAlternative(alternative).Build()); [Fact] public void ImageElementBuilder_WithSize() { foreach (ImageSize size in (ImageSize[])Enum.GetValues(typeof(ImageSize))) { - ImageElementBuilder builder = new ImageElementBuilder().WithSize(size); + ImageElementBuilder builder = new ImageElementBuilder().WithSource("https://www.kookapp.cn/").WithSize(size); Assert.Equal(size, builder.Size); Assert.Equal(size, builder.Build().Size); } } [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("http://kaiheila.net")] - [InlineData("https://kaiheila.net")] + [InlineData("http://www.kookapp.cn/")] + [InlineData("https://www.kookapp.cn/")] public void ImageElementBuilder_AcceptedUrl(string source) { ImageElementBuilder builder = new ImageElementBuilder().WithSource(source); @@ -204,14 +217,16 @@ public void ImageElementBuilder_AcceptedUrl(string source) } [Theory] + [InlineData(null)] + [InlineData("")] [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void ImageElementBuilder_InvalidUrl(string source) + public void ImageElementBuilder_InvalidUrl(string? source) { - ImageElementBuilder builder = new ImageElementBuilder().WithSource(source); + ImageElementBuilder builder = new ImageElementBuilder().WithSource(source!); Assert.Equal(source, builder.Source); - Assert.Throws(() => builder.Build()); + Assert.ThrowsAny(() => builder.Build()); } [Theory] @@ -273,7 +288,7 @@ public void ButtonElementBuilder_ButtonTheme() { foreach (ButtonTheme theme in (ButtonTheme[])Enum.GetValues(typeof(ButtonTheme))) { - ButtonElementBuilder builder = new ButtonElementBuilder().WithTheme(theme); + ButtonElementBuilder builder = new ButtonElementBuilder().WithText("text").WithTheme(theme); Assert.Equal(theme, builder.Theme); Assert.Equal(theme, builder.Build().Theme); } @@ -287,21 +302,22 @@ public void ButtonElementBuilder_ButtonTheme() public void ButtonElementBuilder_ValidText(string text) { ButtonElementBuilder builder = new ButtonElementBuilder().WithText(text); - Assert.Equal(text, ((PlainTextElementBuilder)builder.Text).Content); + Assert.Equal(text, ((PlainTextElementBuilder?)builder.Text)?.Content); Assert.Equal(text, ((PlainTextElement)builder.Build().Text).Content); } [Theory] // 41 characters [InlineData("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNO")] - public void ButtonElementBuilder_InvalidText(string text) => Assert.Throws(() => new ButtonElementBuilder().WithText(text)); + public void ButtonElementBuilder_InvalidText(string text) => + Assert.Throws(() => new ButtonElementBuilder().WithText(text).Build()); [Theory] - [InlineData("http://kaiheila.net")] - [InlineData("https://kaiheila.net")] + [InlineData("http://www.kookapp.cn/")] + [InlineData("https://www.kookapp.cn/")] public void ButtonElementBuilder_AcceptedUrl(string source) { - ButtonElementBuilder builder = new ButtonElementBuilder().WithClick(ButtonClickEventType.Link).WithValue(source); + ButtonElementBuilder builder = new ButtonElementBuilder().WithClick(ButtonClickEventType.Link).WithText("text").WithValue(source); Assert.Equal(ButtonClickEventType.Link, builder.Click); Assert.Equal(ButtonClickEventType.Link, builder.Build().Click); Assert.Equal(source, builder.Value); @@ -314,7 +330,7 @@ public void ButtonElementBuilder_AcceptedUrl(string source) [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void ButtonElementBuilder_InvalidUrl(string source) + public void ButtonElementBuilder_InvalidUrl(string? source) { ButtonElementBuilder builder = new ButtonElementBuilder().WithClick(ButtonClickEventType.Link).WithValue(source); Assert.Equal(ButtonClickEventType.Link, builder.Click); @@ -337,10 +353,11 @@ public void ParagraphStructBuilder_Constructor() builder = new ParagraphStructBuilder { ColumnCount = 3, - Fields = new List - { - new KMarkdownElementBuilder().WithContent("KMarkdown"), new PlainTextElementBuilder().WithContent("PlainText") - } + Fields = + [ + new KMarkdownElementBuilder().WithContent("KMarkdown"), + new PlainTextElementBuilder().WithContent("PlainText") + ] }; Assert.Equal(ElementType.Paragraph, builder.Type); Assert.Equal(ElementType.Paragraph, builder.Build().Type); @@ -367,16 +384,16 @@ public void ParagraphStructBuilder_AddFieldAction() public void ParagraphStructBuilder_InvalidField() { ParagraphStructBuilder builder = new(); - Assert.Throws(() => builder.AddField(b => b.WithSource(Icon))); - Assert.Throws(() => builder.AddField(b => b.WithText("button"))); - Assert.Throws(() => builder = new ParagraphStructBuilder + Assert.Throws(() => builder.AddField(b => b.WithSource(Icon).Build()).Build()); + Assert.Throws(() => builder.AddField(b => b.WithText("button")).Build()); + Assert.Throws(() => new ParagraphStructBuilder { - Fields = new List { new ImageElementBuilder().WithSource(Icon) } - }); - Assert.Throws(() => builder = new ParagraphStructBuilder + Fields = [new ImageElementBuilder().WithSource(Icon)] + }.Build()); + Assert.Throws(() => new ParagraphStructBuilder { - Fields = new List { new ButtonElementBuilder().WithText("button") } - }); + Fields = [new ButtonElementBuilder().WithText("button")] + }.Build()); } [Theory] @@ -385,5 +402,5 @@ public void ParagraphStructBuilder_InvalidField() [InlineData(4)] [InlineData(32)] public void ParagraphStructBuilder_InvalidColumnCount(int columnCount) => - Assert.Throws(() => new ParagraphStructBuilder().WithColumnCount(columnCount)); + Assert.Throws(() => new ParagraphStructBuilder().WithColumnCount(columnCount).Build()); } diff --git a/test/Kook.Net.Tests.Unit/EmojiTests.cs b/test/Kook.Net.Tests.Unit/EmojiTests.cs index df533628..4a428777 100644 --- a/test/Kook.Net.Tests.Unit/EmojiTests.cs +++ b/test/Kook.Net.Tests.Unit/EmojiTests.cs @@ -13,7 +13,7 @@ public class EmojiTests [InlineData(":+1:")] public void Test_Emoji_Parse(string input) { - Assert.True(Emoji.TryParse(input, out Emoji emoji)); + Assert.True(Emoji.TryParse(input, out Emoji? emoji)); Assert.NotNull(emoji); Assert.Equal(emoji.Name, emoji.Id); } @@ -36,7 +36,7 @@ public void Test_Invalid_Emoji_Parse(string input) [InlineData("(emj)两颗骰子(emj)[0/24677/ofbqSypFUx0a00a0]", "两颗骰子", "0/24677/ofbqSypFUx0a00a0", TagMode.KMarkdown)] public void Test_Emote_Parse(string input, string name, string id, TagMode tagMode) { - Assert.True(Emote.TryParse(input, out Emote emote, tagMode)); + Assert.True(Emote.TryParse(input, out Emote? emote, tagMode)); Assert.NotNull(emote); Assert.Equal(name, emote.Name); Assert.Equal(id, emote.Id); diff --git a/test/Kook.Net.Tests.Unit/FormatTests.cs b/test/Kook.Net.Tests.Unit/FormatTests.cs index b9a312a2..359a4ba2 100644 --- a/test/Kook.Net.Tests.Unit/FormatTests.cs +++ b/test/Kook.Net.Tests.Unit/FormatTests.cs @@ -12,17 +12,17 @@ public class FormatTests [InlineData(@"~text~", @"\~text\~")] [InlineData(@"`text`", @"\`text\`")] [InlineData(@"> text", @"\> text")] - public void Sanitize(string input, string expected) => Assert.Equal(expected, Format.Sanitize(input)); + public void Sanitize(string input, string expected) => Assert.Equal(expected, input.Sanitize()); [Fact] public void Code() { // no language - Assert.Equal("`test`", Format.Code("test")); - Assert.Equal("```\nanother\none\n```", Format.Code("another\none")); + Assert.Equal("`test`", "test".Code()); + Assert.Equal("```\nanother\none\n```", "another\none".Code()); // language specified - Assert.Equal("```cs\ntest\n```", Format.Code("test", "cs")); - Assert.Equal("```cs\nanother\none\n```", Format.Code("another\none", "cs")); + Assert.Equal("```cs\ntest\n```", "test".Code("cs")); + Assert.Equal("```cs\nanother\none\n```", "another\none".Code("cs")); } [Fact] @@ -36,10 +36,10 @@ public void Code() // should work with CR or CRLF [InlineData("inb4\ngreentext", "> inb4\ngreentext\n")] [InlineData("inb4\r\ngreentext", "> inb4\r\ngreentext\r\n")] - public void Quote(string input, string expected) => Assert.Equal(expected, Format.Quote(input)); + public void Quote(string input, string expected) => Assert.Equal(expected, input.Quote()); [Theory] - [InlineData(null, null)] + [InlineData(null, "> \u200d")] [InlineData("", "")] [InlineData("\n", "\n")] [InlineData("foo\n\nbar", "> foo\n\u200d\nbar")] @@ -51,7 +51,7 @@ public void Code() [InlineData("inb4\r\rgreentext", "> inb4\r\u200d\rgreentext")] [InlineData("inb4\r\ngreentext", "> inb4\r\ngreentext")] [InlineData("inb4\r\n\r\ngreentext", "> inb4\r\n\u200d\r\ngreentext")] - public void BlockQuote(string input, string expected) => Assert.Equal(expected, Format.BlockQuote(input)); + public void BlockQuote(string? input, string expected) => Assert.Equal(expected, input.BlockQuote()); [Theory] [InlineData("", "")] @@ -66,7 +66,7 @@ public void Code() "berries and Cream, I'm a little lad who loves berries and cream")] public void StripMarkdown(string input, string expected) { - string test = Format.StripMarkDown(input); + string test = input.StripMarkDown(); Assert.Equal(expected, test); } } diff --git a/test/Kook.Net.Tests.Unit/Kook.Net.Tests.Unit.csproj b/test/Kook.Net.Tests.Unit/Kook.Net.Tests.Unit.csproj index 919bd72d..136826e1 100644 --- a/test/Kook.Net.Tests.Unit/Kook.Net.Tests.Unit.csproj +++ b/test/Kook.Net.Tests.Unit/Kook.Net.Tests.Unit.csproj @@ -1,37 +1,5 @@ - - net8.0 - disable - false - Kook.Net.Tests - default - NU1803 - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - + diff --git a/test/Kook.Net.Tests.Unit/MentionUtilsTests.cs b/test/Kook.Net.Tests.Unit/MentionUtilsTests.cs index 6995b40d..b8782b88 100644 --- a/test/Kook.Net.Tests.Unit/MentionUtilsTests.cs +++ b/test/Kook.Net.Tests.Unit/MentionUtilsTests.cs @@ -78,10 +78,10 @@ public void ParseUser_Fail(string user, TagMode tagMode) [Fact] public void ParseUser_Null() { - Assert.Throws(() => MentionUtils.ParseUser(null, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.ParseUser(null, TagMode.KMarkdown)); - Assert.Throws(() => MentionUtils.TryParseUser(null, out _, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.TryParseUser(null, out _, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.ParseUser(null!, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.ParseUser(null!, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.TryParseUser(null!, out _, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.TryParseUser(null!, out _, TagMode.KMarkdown)); } [Theory] @@ -114,10 +114,10 @@ public void ParseChannel_Fail(string channel, TagMode tagMode) [Fact] public void ParseChannel_Null() { - Assert.Throws(() => MentionUtils.ParseChannel(null, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.ParseChannel(null, TagMode.KMarkdown)); - Assert.Throws(() => MentionUtils.TryParseChannel(null, out _, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.TryParseChannel(null, out _, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.ParseChannel(null!, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.ParseChannel(null!, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.TryParseChannel(null!, out _, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.TryParseChannel(null!, out _, TagMode.KMarkdown)); } [Theory] @@ -150,9 +150,9 @@ public void ParseRole_Fail(string role, TagMode tagMode) [Fact] public void ParseRole_Null() { - Assert.Throws(() => MentionUtils.ParseRole(null, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.ParseRole(null, TagMode.KMarkdown)); - Assert.Throws(() => MentionUtils.TryParseRole(null, out _, TagMode.PlainText)); - Assert.Throws(() => MentionUtils.TryParseRole(null, out _, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.ParseRole(null!, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.ParseRole(null!, TagMode.KMarkdown)); + Assert.Throws(() => MentionUtils.TryParseRole(null!, out _, TagMode.PlainText)); + Assert.Throws(() => MentionUtils.TryParseRole(null!, out _, TagMode.KMarkdown)); } } diff --git a/test/Kook.Net.Tests.Unit/MessageHelperTests.cs b/test/Kook.Net.Tests.Unit/MessageHelperTests.cs index 76b16092..061534a2 100644 --- a/test/Kook.Net.Tests.Unit/MessageHelperTests.cs +++ b/test/Kook.Net.Tests.Unit/MessageHelperTests.cs @@ -29,7 +29,7 @@ public class MessageHelperTests "` (met)all(met) (met)here(met) (met)1896684851(met) (rol)1896684851(rol) (chn)1896684851(chn) (emj)test(emj)[1990044438283387/aIVQrtPv4z10b10b] `")] public void ParseTagsInCode(string testData) { - ImmutableArray result = MessageHelper.ParseTags(testData, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(testData, null, null, [], TagMode.KMarkdown); Assert.Empty(result); } @@ -43,7 +43,7 @@ public void ParseTagsInCode(string testData) [InlineData("``` code ``` (met)here(met) ``` more ```")] public void ParseTagsAroundCode(string testData) { - ImmutableArray result = MessageHelper.ParseTags(testData, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(testData, null, null, [], TagMode.KMarkdown); Assert.NotEmpty(result); } @@ -53,7 +53,7 @@ public void ParseTagsAroundCode(string testData) [InlineData(@"hey\`\`\`(met)all(met)\`\`\`!!")] public void IgnoreEscapedCodeBlocks(string testData) { - ImmutableArray result = MessageHelper.ParseTags(testData, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(testData, null, null, [], TagMode.KMarkdown); Assert.NotEmpty(result); } @@ -68,7 +68,7 @@ public void IgnoreEscapedCodeBlocks(string testData) [InlineData("<>(rol)1896684851(rol)")] public void ParseRole(string roleTag) { - ImmutableArray result = MessageHelper.ParseTags(roleTag, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(roleTag, null, null, [], TagMode.KMarkdown); Assert.Contains(result, x => x.Type == TagType.RoleMention); } @@ -79,7 +79,7 @@ public void ParseRole(string roleTag) [InlineData("<>(chn)1896684851(chn)")] public void ParseChannel(string channelTag) { - ImmutableArray result = MessageHelper.ParseTags(channelTag, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(channelTag, null, null, [], TagMode.KMarkdown); Assert.Contains(result, x => x.Type == TagType.ChannelMention); } @@ -91,7 +91,7 @@ public void ParseChannel(string channelTag) [InlineData("(emj)一颗骰子(emj)[0/24677/OaRgXkJ8gB0a00a0]")] public void ParseEmoji(string emoji) { - ImmutableArray result = MessageHelper.ParseTags(emoji, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(emoji, null, null, [], TagMode.KMarkdown); Assert.Contains(result, x => x.Type == TagType.Emoji); } @@ -101,7 +101,7 @@ public void ParseEmoji(string emoji) [InlineData("**(met)all(met)**")] public void ParseEveryone(string everyone) { - ImmutableArray result = MessageHelper.ParseTags(everyone, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(everyone, null, null, [], TagMode.KMarkdown); Assert.Contains(result, x => x.Type == TagType.EveryoneMention); } @@ -111,7 +111,7 @@ public void ParseEveryone(string everyone) [InlineData("**(met)here(met)**")] public void ParseHere(string here) { - ImmutableArray result = MessageHelper.ParseTags(here, null, null, null, TagMode.KMarkdown); + ImmutableArray result = MessageHelper.ParseTags(here, null, null, [], TagMode.KMarkdown); Assert.Contains(result, x => x.Type == TagType.HereMention); } } diff --git a/test/Kook.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs b/test/Kook.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs index 024f9e03..1fb78432 100644 --- a/test/Kook.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs +++ b/test/Kook.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs @@ -24,37 +24,37 @@ internal sealed class MockedCategoryChannel : ICategoryChannel public IReadOnlyCollection UserPermissionOverwrites => throw new NotImplementedException(); - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null) => throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null) => throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => throw new NotImplementedException(); - public Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null) => + public Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions? options = null) => throw new NotImplementedException(); public Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options = null) => throw new NotImplementedException(); + RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => throw new NotImplementedException(); - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => GetUsersAsync(mode, options); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => GetUsersAsync(mode, options); - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => throw new NotImplementedException(); - public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions? options = null) => throw new NotImplementedException(); } diff --git a/test/Kook.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Kook.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 104593ef..4ec23774 100644 --- a/test/Kook.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Kook.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -9,91 +9,131 @@ internal sealed class MockedDMChannel : IDMChannel { ulong IEntity.Id => throw new NotImplementedException(); + /// public Guid ChatCode => throw new NotImplementedException(); + /// public IUser Recipient => throw new NotImplementedException(); - public Task CloseAsync(RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task CloseAsync(RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, - RequestOptions options = null) => + /// + public Task> SendFileAsync( + string path, string? filename = null, AttachmentType type = AttachmentType.File, + IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(Stream stream, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, RequestOptions options = null) => + /// + public Task> SendFileAsync( + Stream stream, string filename, AttachmentType type = AttachmentType.File, + IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(FileAttachment attachment, IQuote quote = null, RequestOptions options = null) => + /// + public Task> SendFileAsync( + FileAttachment attachment, IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendTextAsync(string text, IQuote quote = null, RequestOptions options = null) => + /// + public Task> SendTextAsync( + string text, IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendCardAsync(ICard card, IQuote quote = null, - RequestOptions options = null) => + /// + public Task> SendCardAsync( + ICard card, IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, RequestOptions options = null) => + /// + public Task> SendCardsAsync( + IEnumerable cards, IQuote? quote = null, RequestOptions? options = null) => throw new NotImplementedException(); + Guid IEntity.Id => throw new NotImplementedException(); + + Guid IDMChannel.Id => throw new NotImplementedException(); + + /// public string Name => throw new NotImplementedException(); - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + /// + public IAsyncEnumerable> GetUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + /// + public Task GetUserAsync(ulong id, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + /// + public Task> SendFileAsync( + string path, string? filename = null, AttachmentType type = AttachmentType.File, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(Stream stream, string fileName, AttachmentType type = AttachmentType.File, - IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + /// + public Task> SendFileAsync( + Stream stream, string filename, AttachmentType type = AttachmentType.File, IQuote? quote = null, + IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(FileAttachment attachment, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => throw new NotImplementedException(); - - public Task> SendTextAsync(string text, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task> SendFileAsync( + FileAttachment attachment, IQuote? quote = null, + IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task> SendCardAsync(ICard card, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task> SendTextAsync( + string text, IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task> SendCardAsync( + ICard card, IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task GetMessageAsync(Guid id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + /// + public Task> SendCardsAsync( + IEnumerable cards, IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + /// + public Task GetMessageAsync(Guid id, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, + /// + public IAsyncEnumerable> GetMessagesAsync( int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) => + RequestOptions? options = null) => throw new NotImplementedException(); + + /// + public IAsyncEnumerable> GetMessagesAsync( + Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, - int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) => + /// + public IAsyncEnumerable> GetMessagesAsync( + IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) => + /// + public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions? options = null) => throw new NotImplementedException(); + /// public IReadOnlyCollection Recipients => throw new NotImplementedException(); - - Guid IEntity.Id => throw new NotImplementedException(); - - Guid IDMChannel.Id => throw new NotImplementedException(); } diff --git a/test/Kook.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs b/test/Kook.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs index 2ac3ad66..080c1890 100644 --- a/test/Kook.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs +++ b/test/Kook.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs @@ -15,9 +15,11 @@ internal sealed class MockedInvalidChannel : IChannel public ulong Id => throw new NotImplementedException(); - public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetUserAsync(ulong id, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); } diff --git a/test/Kook.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Kook.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index fef53d7c..326d24c6 100644 --- a/test/Kook.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Kook.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -23,36 +23,48 @@ internal sealed class MockedTextChannel : ITextChannel public IReadOnlyCollection UserPermissionOverwrites => throw new NotImplementedException(); - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null) => + public Task ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options = null) => throw new NotImplementedException(); - public Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options = null) => + throw new NotImplementedException(); - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + throw new NotImplementedException(); - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => GetUsersAsync(mode, options); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + GetUsersAsync(mode, options); - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + throw new NotImplementedException(); - public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions? options = null) => + throw new NotImplementedException(); public ulong? CategoryId => throw new NotImplementedException(); @@ -61,80 +73,97 @@ public IAsyncEnumerable> GetUsersAsync(CacheMode public ulong CreatorId => throw new NotImplementedException(); /// - public Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task SyncPermissionsAsync(RequestOptions? options = null) => + throw new NotImplementedException(); - public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null) => throw new NotImplementedException(); - public Task> GetInvitesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetInvitesAsync(RequestOptions? options = null) => + throw new NotImplementedException(); - public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) => + public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => throw new NotImplementedException(); - public Task CreateInviteAsync(int? maxAge, int? maxUses = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, + RequestOptions? options = null) => + throw new NotImplementedException(); public string PlainTextMention => throw new NotImplementedException(); public string KMarkdownMention => throw new NotImplementedException(); - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, - IQuote quote = null, - IUser ephemeralUser = null, RequestOptions options = null) => + public Task> SendFileAsync(string path, + string? filename = null, AttachmentType type = AttachmentType.File, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(Stream stream, string fileName, AttachmentType type = AttachmentType.File, - IQuote quote = null, - IUser ephemeralUser = null, RequestOptions options = null) => + public Task> SendFileAsync(Stream stream, + string filename, AttachmentType type = AttachmentType.File, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendFileAsync(FileAttachment attachment, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + public Task> SendFileAsync(FileAttachment attachment, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendTextAsync(string text, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + public Task> SendTextAsync(string text, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> SendCardAsync(ICard card, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => throw new NotImplementedException(); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) => + throw new NotImplementedException(); - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetMessageAsync(Guid id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetMessageAsync(Guid id, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, + public IAsyncEnumerable> GetMessagesAsync( + Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) => + RequestOptions? options = null) => throw new NotImplementedException(); - public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, + public IAsyncEnumerable> GetMessagesAsync( + IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) => + RequestOptions? options = null) => throw new NotImplementedException(); - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) => + public Task ModifyMessageAsync(Guid messageId, Action func, + RequestOptions? options = null) => throw new NotImplementedException(); public string Topic => throw new NotImplementedException(); public int SlowModeInterval => throw new NotImplementedException(); - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task> GetPinnedMessagesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetPinnedMessagesAsync(RequestOptions? options = null) => + throw new NotImplementedException(); } diff --git a/test/Kook.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Kook.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index fc72e0c3..cfe8cd71 100644 --- a/test/Kook.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Kook.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -24,36 +24,47 @@ internal sealed class MockedVoiceChannel : IVoiceChannel public IReadOnlyCollection UserPermissionOverwrites => throw new NotImplementedException(); - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IGuildUser user, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task ModifyPermissionOverwriteAsync(IRole role, Func func, RequestOptions options = null) => + public Task ModifyPermissionOverwriteAsync(IRole role, + Func func, RequestOptions? options = null) => throw new NotImplementedException(); - public Task ModifyPermissionOverwriteAsync(IGuildUser user, Func func, - RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyPermissionOverwriteAsync(IGuildUser user, + Func func, RequestOptions? options = null) => + throw new NotImplementedException(); - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + throw new NotImplementedException(); - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => GetUsersAsync(mode, options); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions? options) => + GetUsersAsync(mode, options); - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions? options) => + throw new NotImplementedException(); - public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions? options = null) => throw new NotImplementedException(); public ulong? CategoryId => throw new NotImplementedException(); @@ -62,21 +73,25 @@ public IAsyncEnumerable> GetUsersAsync(CacheMode public ulong CreatorId => throw new NotImplementedException(); /// - public Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task SyncPermissionsAsync(RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetCategoryAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task GetCreatorAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); - public Task> GetInvitesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetInvitesAsync(RequestOptions? options = null) => + throw new NotImplementedException(); - public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, InviteMaxUses maxUses = InviteMaxUses.Unlimited, - RequestOptions options = null) => + public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._604800, + InviteMaxUses maxUses = InviteMaxUses.Unlimited, RequestOptions? options = null) => throw new NotImplementedException(); - public Task CreateInviteAsync(int? maxAge, int? maxUses = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteAsync(int? maxAge = 604800, int? maxUses = null, + RequestOptions? options = null) => + throw new NotImplementedException(); public string PlainTextMention => throw new NotImplementedException(); @@ -84,7 +99,7 @@ public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._60480 public VoiceQuality? VoiceQuality => throw new NotImplementedException(); - public int? UserLimit => throw new NotImplementedException(); + public int UserLimit => throw new NotImplementedException(); /// public bool? IsVoiceRegionOverwritten => throw new NotImplementedException(); @@ -97,66 +112,87 @@ public Task CreateInviteAsync(InviteMaxAge maxAge = InviteMaxAge._60480 public bool HasPassword => throw new NotImplementedException(); /// - public Task ConnectAsync(/*bool selfDeaf = false, bool selfMute = false, */bool external = false, bool disconnect = true) => throw new NotImplementedException(); + public Task ConnectAsync( /*bool selfDeaf = false, bool selfMute = false, */ + bool external = false, bool disconnect = true) => + throw new NotImplementedException(); /// public Task DisconnectAsync() => throw new NotImplementedException(); - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + throw new NotImplementedException(); - public Task> GetConnectedUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public Task> GetConnectedUsersAsync( + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); /// - public Task> SendFileAsync(string path, string fileName = null, AttachmentType type = AttachmentType.File, IQuote quote = null, - IUser ephemeralUser = null, RequestOptions options = null) => + public Task> SendFileAsync(string path, + string? filename = null, AttachmentType type = AttachmentType.File, IQuote? quote = null, + IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); /// - public Task> SendFileAsync(Stream stream, string fileName, AttachmentType type = AttachmentType.File, IQuote quote = null, - IUser ephemeralUser = null, RequestOptions options = null) => + public Task> SendFileAsync(Stream stream, + string filename, AttachmentType type = AttachmentType.File, IQuote? quote = null, + IUser? ephemeralUser = null, RequestOptions? options = null) => throw new NotImplementedException(); /// - public Task> SendFileAsync(FileAttachment attachment, IQuote quote = null, IUser ephemeralUser = null, - RequestOptions options = null) => + public Task> SendFileAsync(FileAttachment attachment, + IQuote? quote = null, IUser? ephemeralUser = null, + RequestOptions? options = null) => throw new NotImplementedException(); /// - public Task> SendTextAsync(string text, IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task> SendTextAsync(string text, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task> SendCardAsync(ICard card, IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task> SendCardAsync(ICard card, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task> SendCardsAsync(IEnumerable cards, IQuote quote = null, IUser ephemeralUser = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task> SendCardsAsync(IEnumerable cards, + IQuote? quote = null, IUser? ephemeralUser = null, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task GetMessageAsync(Guid id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task GetMessageAsync(Guid id, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public IAsyncEnumerable> GetMessagesAsync(int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, - RequestOptions options = null) => + public IAsyncEnumerable> GetMessagesAsync( + int limit = KookConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, + RequestOptions? options = null) => throw new NotImplementedException(); /// - public IAsyncEnumerable> GetMessagesAsync(Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetMessagesAsync( + Guid referenceMessageId, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); /// - public IAsyncEnumerable> GetMessagesAsync(IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => + public IAsyncEnumerable> GetMessagesAsync( + IMessage referenceMessage, Direction dir, int limit = KookConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions? options = null) => throw new NotImplementedException(); /// - public Task DeleteMessageAsync(Guid messageId, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(Guid messageId, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(IMessage message, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyMessageAsync(Guid messageId, Action func, RequestOptions? options = null) => + throw new NotImplementedException(); /// public string Topic => throw new NotImplementedException(); @@ -165,8 +201,10 @@ public IAsyncEnumerable> GetMessagesAsync(IMessage public int SlowModeInterval => throw new NotImplementedException(); /// - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions? options = null) => + throw new NotImplementedException(); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetPinnedMessagesAsync(RequestOptions? options = null) => + throw new NotImplementedException(); } diff --git a/test/Kook.Net.Tests.Unit/ModuleBuilderTests.cs b/test/Kook.Net.Tests.Unit/ModuleBuilderTests.cs index e4757db3..9dfe1227 100644 --- a/test/Kook.Net.Tests.Unit/ModuleBuilderTests.cs +++ b/test/Kook.Net.Tests.Unit/ModuleBuilderTests.cs @@ -20,12 +20,12 @@ public void HeaderModuleBuilder_Constructor() Assert.Equal(ModuleType.Header, builder.Type); Assert.Equal(ModuleType.Header, builder.Build().Type); Assert.Equal("text", builder.Text.Content); - Assert.Equal("text", builder.Build().Text.Content); + Assert.Equal("text", builder.Build().Text?.Content); builder = new HeaderModuleBuilder { Text = new PlainTextElementBuilder().WithContent("content") }; Assert.Equal(ModuleType.Header, builder.Type); Assert.Equal(ModuleType.Header, builder.Build().Type); Assert.Equal("content", builder.Text.Content); - Assert.Equal("content", builder.Build().Text.Content); + Assert.Equal("content", builder.Build().Text?.Content); } [Fact] @@ -36,7 +36,7 @@ public void HeaderModuleBuilder_WithTextAction() Assert.IsType(builder.Text); Assert.IsType(builder.Build().Text); Assert.Equal("text", builder.Text.Content); - Assert.Equal("text", builder.Build().Text.Content); + Assert.Equal("text", builder.Build().Text?.Content); } [Fact] @@ -54,24 +54,27 @@ IEnumerable GetValidContent() { HeaderModuleBuilder builder = new HeaderModuleBuilder() .WithText(new PlainTextElementBuilder().WithContent(text)); - Assert.Equal(text, builder.Text.Content); - Assert.Equal(text, builder.Build().Text.Content); + Assert.Equal(text, builder.Text?.Content); + Assert.Equal(text, builder.Build().Text?.Content); } } [Fact] public void HeaderModuleBuilder_InvalidText() { - IEnumerable GetInvalidContent() + IEnumerable GetInvalidContent() { yield return null; yield return new string('a', 101); } - foreach (string text in GetInvalidContent()) + foreach (string? text in GetInvalidContent()) { Assert.Throws(() => new HeaderModuleBuilder() - .WithText(b => b.WithContent(text))); + .WithText(b => + { + b.Content = text; + }).Build()); } } @@ -80,8 +83,8 @@ IEnumerable GetInvalidContent() public void HeaderModuleBuilder_ImplicitConversion(string text) { HeaderModuleBuilder builder = text; - Assert.Equal(text, builder.Text.Content); - Assert.Equal(text, builder.Build().Text.Content); + Assert.Equal(text, builder.Text?.Content); + Assert.Equal(text, builder.Build().Text?.Content); } [Fact] @@ -93,10 +96,10 @@ public void SectionModuleBuilder_Constructor() .WithAccessory(new ImageElementBuilder().WithSource(Icon)); Assert.Equal(ModuleType.Section, builder.Type); Assert.Equal(ModuleType.Section, builder.Build().Type); - Assert.Equal("text", ((PlainTextElementBuilder)builder.Text).Content); - Assert.Equal("text", ((PlainTextElement)builder.Build().Text).Content); - Assert.Equal(Icon, ((ImageElementBuilder)builder.Accessory).Source); - Assert.Equal(Icon, ((ImageElement)builder.Build().Accessory).Source); + Assert.Equal("text", ((PlainTextElementBuilder?)builder.Text)?.Content); + Assert.Equal("text", ((PlainTextElement?)builder.Build().Text)?.Content); + Assert.Equal(Icon, ((ImageElementBuilder?)builder.Accessory)?.Source); + Assert.Equal(Icon, ((ImageElement?)builder.Build().Accessory)?.Source); builder = new SectionModuleBuilder { Mode = SectionAccessoryMode.Right, @@ -106,9 +109,9 @@ public void SectionModuleBuilder_Constructor() Assert.Equal(ModuleType.Section, builder.Type); Assert.Equal(ModuleType.Section, builder.Build().Type); Assert.Equal("content", ((KMarkdownElementBuilder)builder.Text).Content); - Assert.Equal("content", ((KMarkdownElement)builder.Build().Text).Content); - Assert.Equal("button", ((KMarkdownElementBuilder)((ButtonElementBuilder)builder.Accessory).Text).Content); - Assert.Equal("button", ((KMarkdownElement)((ButtonElement)builder.Build().Accessory).Text).Content); + Assert.Equal("content", ((KMarkdownElement?)builder.Build().Text)?.Content); + Assert.Equal("button", ((KMarkdownElementBuilder?)((ButtonElementBuilder)builder.Accessory).Text)?.Content); + Assert.Equal("button", ((KMarkdownElement?)((ButtonElement?)builder.Build().Accessory)?.Text)?.Content); } [Fact] @@ -119,7 +122,7 @@ public void SectionModuleBuilder_WithTextAction() Assert.IsType(builder.Text); Assert.IsType(builder.Build().Text); Assert.Equal("text", ((PlainTextElementBuilder)builder.Text).Content); - Assert.Equal("text", ((PlainTextElement)builder.Build().Text).Content); + Assert.Equal("text", ((PlainTextElement?)builder.Build().Text)?.Content); } [Fact] @@ -130,27 +133,27 @@ public void SectionModuleBuilder_WithAccessoryAction() Assert.IsType(builder.Accessory); Assert.IsType(builder.Build().Accessory); Assert.Equal(Icon, ((ImageElementBuilder)builder.Accessory).Source); - Assert.Equal(Icon, ((ImageElement)builder.Build().Accessory).Source); + Assert.Equal(Icon, ((ImageElement?)builder.Build().Accessory)?.Source); } [Fact] public void SectionModuleBuilder_InvalidText() { SectionModuleBuilder builder = new(); - Assert.Throws(() => builder.WithText(b => b.WithSource(Icon))); - Assert.Throws(() => builder.WithText(b => b.WithText("button"))); - Assert.Throws(() => builder = new SectionModuleBuilder { Text = new ImageElementBuilder().WithSource(Icon) }); - Assert.Throws(() => builder = new SectionModuleBuilder { Text = new ButtonElementBuilder().WithText("button") }); + Assert.Throws(() => builder.WithText(b => b.WithSource(Icon)).Build()); + Assert.Throws(() => builder.WithText(b => b.WithText("button")).Build()); + Assert.Throws(() => new SectionModuleBuilder { Text = new ImageElementBuilder().WithSource(Icon) }.Build()); + Assert.Throws(() => new SectionModuleBuilder { Text = new ButtonElementBuilder().WithText("button") }.Build()); } [Fact] public void SectionModuleBuilder_InvalidAccessory() { SectionModuleBuilder builder = new(); - Assert.Throws(() => builder.WithAccessory(b => b.WithContent(Name))); - Assert.Throws(() => builder.WithAccessory(b => b.WithContent(Name))); - Assert.Throws(() => builder = new SectionModuleBuilder { Accessory = new PlainTextElementBuilder().WithContent(Name) }); - Assert.Throws(() => builder = new SectionModuleBuilder { Accessory = new KMarkdownElementBuilder().WithContent(Name) }); + Assert.Throws(() => builder.WithAccessory(b => b.WithContent(Name)).Build()); + Assert.Throws(() => builder.WithAccessory(b => b.WithContent(Name)).Build()); + Assert.Throws(() => new SectionModuleBuilder { Accessory = new PlainTextElementBuilder().WithContent(Name) }.Build()); + Assert.Throws(() => new SectionModuleBuilder { Accessory = new KMarkdownElementBuilder().WithContent(Name) }.Build()); } [Fact] @@ -171,7 +174,7 @@ public void ImageGroupBuilder_Constructor() Assert.Equal(ModuleType.ImageGroup, builder.Build().Type); Assert.Equal(Icon, builder.Elements[0].Source); Assert.Equal(Icon, builder.Build().Elements[0].Source); - builder = new ImageGroupModuleBuilder { Elements = new List { new ImageElementBuilder().WithSource(Icon) } }; + builder = new ImageGroupModuleBuilder { Elements = [new ImageElementBuilder().WithSource(Icon)] }; Assert.Equal(ModuleType.ImageGroup, builder.Type); Assert.Equal(ModuleType.ImageGroup, builder.Build().Type); Assert.Equal(Icon, builder.Elements[0].Source); @@ -193,7 +196,8 @@ public void ImageGroupBuilder_AddElementAction() public void ImageGroupBuilder_ValidElements() { ImageGroupModuleBuilder builder = new(); - for (int i = 0; i < 9; i++) builder.AddElement(b => b.WithSource(Icon)); + for (int i = 0; i < 9; i++) + builder.AddElement(b => b.WithSource(Icon)); Assert.Equal(9, builder.Elements.Count); builder = new ImageGroupModuleBuilder { Elements = Enumerable.Repeat(new ImageElementBuilder().WithSource(Icon), 9).ToList() }; @@ -204,13 +208,14 @@ public void ImageGroupBuilder_ValidElements() public void ImageGroupBuilder_InvalidElements() { ImageGroupModuleBuilder builder = new(); - for (int i = 0; i < 9; i++) builder.AddElement(b => b.WithSource(Icon)); + for (int i = 0; i < 9; i++) + builder.AddElement(b => b.WithSource(Icon)); - Assert.Throws(() => builder.AddElement(b => b.WithSource(Icon))); - Assert.Throws(() => builder = new ImageGroupModuleBuilder + Assert.Throws(() => builder.AddElement(b => b.WithSource(Icon)).Build()); + Assert.Throws(() => new ImageGroupModuleBuilder { Elements = Enumerable.Repeat(new ImageElementBuilder().WithSource(Icon), 10).ToList() - }); + }.Build()); } [Fact] @@ -222,7 +227,7 @@ public void ContainerBuilder_Constructor() Assert.Equal(ModuleType.Container, builder.Build().Type); Assert.Equal(Icon, builder.Elements[0].Source); Assert.Equal(Icon, builder.Build().Elements[0].Source); - builder = new ContainerModuleBuilder { Elements = new List { new ImageElementBuilder().WithSource(Icon) } }; + builder = new ContainerModuleBuilder { Elements = [new ImageElementBuilder().WithSource(Icon)] }; Assert.Equal(ModuleType.Container, builder.Type); Assert.Equal(ModuleType.Container, builder.Build().Type); Assert.Equal(Icon, builder.Elements[0].Source); @@ -244,7 +249,8 @@ public void ContainerBuilder_AddElementAction() public void ContainerBuilder_ValidElements() { ContainerModuleBuilder builder = new(); - for (int i = 0; i < 9; i++) builder.AddElement(b => b.WithSource(Icon)); + for (int i = 0; i < 9; i++) + builder.AddElement(b => b.WithSource(Icon)); Assert.Equal(9, builder.Elements.Count); builder = new ContainerModuleBuilder { Elements = Enumerable.Repeat(new ImageElementBuilder().WithSource(Icon), 9).ToList() }; @@ -255,13 +261,14 @@ public void ContainerBuilder_ValidElements() public void ContainerBuilder_InvalidElements() { ContainerModuleBuilder builder = new(); - for (int i = 0; i < 9; i++) builder.AddElement(b => b.WithSource(Icon)); + for (int i = 0; i < 9; i++) + builder.AddElement(b => b.WithSource(Icon)); - Assert.Throws(() => builder.AddElement(b => b.WithSource(Icon))); - Assert.Throws(() => builder = new ContainerModuleBuilder + Assert.Throws(() => builder.AddElement(b => b.WithSource(Icon)).Build()); + Assert.Throws(() => new ContainerModuleBuilder { Elements = Enumerable.Repeat(new ImageElementBuilder().WithSource(Icon), 10).ToList() - }); + }.Build()); } [Fact] @@ -271,12 +278,12 @@ public void ActionGroupBuilder_Constructor() .AddElement(b => b.WithText(Name)); Assert.Equal(ModuleType.ActionGroup, builder.Type); Assert.Equal(ModuleType.ActionGroup, builder.Build().Type); - Assert.Equal(Name, ((PlainTextElementBuilder)builder.Elements[0].Text).Content); + Assert.Equal(Name, ((PlainTextElementBuilder?)builder.Elements[0].Text)?.Content); Assert.Equal(Name, ((PlainTextElement)builder.Build().Elements[0].Text).Content); - builder = new ActionGroupModuleBuilder { Elements = new List { new ButtonElementBuilder().WithText(Name, true) } }; + builder = new ActionGroupModuleBuilder { Elements = [new ButtonElementBuilder().WithText(Name, true)] }; Assert.Equal(ModuleType.ActionGroup, builder.Type); Assert.Equal(ModuleType.ActionGroup, builder.Build().Type); - Assert.Equal(Name, ((KMarkdownElementBuilder)builder.Elements[0].Text).Content); + Assert.Equal(Name, ((KMarkdownElementBuilder?)builder.Elements[0].Text)?.Content); Assert.Equal(Name, ((KMarkdownElement)builder.Build().Elements[0].Text).Content); } @@ -287,7 +294,7 @@ public void ActionGroupBuilder_AddElementAction() .AddElement(b => b.WithText(Name)); Assert.IsType(builder.Elements[0]); Assert.IsType(builder.Build().Elements[0]); - Assert.Equal(Name, ((PlainTextElementBuilder)builder.Elements[0].Text).Content); + Assert.Equal(Name, ((PlainTextElementBuilder?)builder.Elements[0].Text)?.Content); Assert.Equal(Name, ((PlainTextElement)builder.Build().Elements[0].Text).Content); } @@ -308,11 +315,11 @@ public void ActionGroupBuilder_InvalidElements() ActionGroupModuleBuilder builder = new(); for (int i = 0; i < 4; i++) builder.AddElement(b => b.WithText(Name)); - Assert.Throws(() => builder.AddElement(b => b.WithText(Name))); - Assert.Throws(() => builder = new ActionGroupModuleBuilder + Assert.Throws(() => builder.AddElement(b => b.WithText(Name)).Build()); + Assert.Throws(() => new ActionGroupModuleBuilder { Elements = Enumerable.Repeat(new ButtonElementBuilder().WithText(Name), 5).ToList() - }); + }.Build()); } [Fact] @@ -324,7 +331,7 @@ public void ContextBuilder_Constructor() Assert.Equal(ModuleType.Context, builder.Build().Type); Assert.Equal(Name, ((PlainTextElementBuilder)builder.Elements[0]).Content); Assert.Equal(Name, ((PlainTextElement)builder.Build().Elements[0]).Content); - builder = new ContextModuleBuilder { Elements = new List { new KMarkdownElementBuilder().WithContent(Name) } }; + builder = new ContextModuleBuilder { Elements = [new KMarkdownElementBuilder().WithContent(Name)] }; Assert.Equal(ModuleType.Context, builder.Type); Assert.Equal(ModuleType.Context, builder.Build().Type); Assert.Equal(Name, ((KMarkdownElementBuilder)builder.Elements[0]).Content); @@ -360,15 +367,16 @@ public void ContextBuilder_ValidElements() public void ContextBuilder_InvalidElements() { ContextModuleBuilder builder = new(); - Assert.Throws(() => builder.AddElement(b => b.WithColumnCount(2))); - Assert.Throws(() => builder.AddElement(b => b.WithText("button"))); - for (int i = 0; i < 10; i++) builder.AddElement(b => b.WithContent(Name)); + Assert.Throws(() => builder.AddElement(b => b.WithColumnCount(2)).Build()); + Assert.Throws(() => builder.AddElement(b => b.WithText("button")).Build()); + for (int i = 0; i < 10; i++) + builder.AddElement(b => b.WithContent(Name)); - Assert.Throws(() => builder.AddElement(b => b.WithContent(Name))); - Assert.Throws(() => builder = new ContextModuleBuilder + Assert.Throws(() => builder.AddElement(b => b.WithContent(Name)).Build()); + Assert.Throws(() => new ContextModuleBuilder { Elements = Enumerable.Repeat(new ButtonElementBuilder().WithText(Name) as IElementBuilder, 11).ToList() - }); + }.Build()); } [Fact] @@ -406,9 +414,9 @@ public void FileModuleBuilder_Constructor() [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void FileModuleBuilder_InvalidUrl(string source) + public void FileModuleBuilder_InvalidUrl(string? source) { - FileModuleBuilder builder = new FileModuleBuilder().WithSource(source); + FileModuleBuilder builder = new FileModuleBuilder().WithSource(source!); Assert.Equal(source, builder.Source); Assert.ThrowsAny(() => builder.Build()); } @@ -440,9 +448,9 @@ public void VideoModuleBuilder_Constructor() [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void VideoModuleBuilder_InvalidUrl(string source) + public void VideoModuleBuilder_InvalidUrl(string? source) { - VideoModuleBuilder builder = new VideoModuleBuilder().WithSource(source); + VideoModuleBuilder builder = new VideoModuleBuilder().WithSource(source!); Assert.Equal(source, builder.Source); Assert.ThrowsAny(() => builder.Build()); } @@ -479,7 +487,7 @@ public void AudioModuleBuilder_Constructor() [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void AudioModuleBuilder_InvalidSource(string source) + public void AudioModuleBuilder_InvalidSource(string? source) { AudioModuleBuilder builder = new AudioModuleBuilder().WithSource(Url).WithCover(Icon); Assert.Equal(Url, builder.Source); @@ -512,19 +520,25 @@ public void CountdownModuleBuilder_Constructor() .WithMode(CountdownMode.Day) .WithEndTime(DateTimeOffset.Now.AddDays(1)); Assert.Equal(ModuleType.Countdown, builder.Type); - Assert.Equal(ModuleType.Countdown, builder.Build().Type); + CountdownModule module = builder.Build(); + Assert.Equal(ModuleType.Countdown, module.Type); Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, builder.EndTime.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, builder.Build().EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, module.EndTime.DateTime, TimeSpan.FromSeconds(1)); builder = new CountdownModuleBuilder { - Mode = CountdownMode.Second, EndTime = DateTimeOffset.Now.AddHours(1), StartTime = DateTimeOffset.Now + Mode = CountdownMode.Second, + EndTime = DateTimeOffset.Now.AddHours(1), + StartTime = DateTimeOffset.Now }; + module = builder.Build(); Assert.Equal(ModuleType.Countdown, builder.Type); - Assert.Equal(ModuleType.Countdown, builder.Build().Type); + Assert.Equal(ModuleType.Countdown, module.Type); Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, builder.EndTime.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, builder.Build().EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, module.EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.NotNull(builder.StartTime); Assert.Equal(DateTimeOffset.Now.DateTime, builder.StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.DateTime, builder.Build().StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); + Assert.NotNull(module.StartTime); + Assert.Equal(DateTimeOffset.Now.DateTime, module.StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); } [Fact] @@ -533,21 +547,26 @@ public void CountdownModuleBuilder_ValidTimes() CountdownModuleBuilder builder = new CountdownModuleBuilder() .WithMode(CountdownMode.Day) .WithEndTime(DateTimeOffset.Now.AddDays(1)); + CountdownModule module = builder.Build(); Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, builder.EndTime.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, builder.Build().EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.Equal(DateTimeOffset.Now.AddDays(1).DateTime, module.EndTime.DateTime, TimeSpan.FromSeconds(1)); builder = new CountdownModuleBuilder() .WithMode(CountdownMode.Hour) .WithEndTime(DateTimeOffset.Now.AddHours(1)); + module = builder.Build(); Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, builder.EndTime.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, builder.Build().EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.Equal(DateTimeOffset.Now.AddHours(1).DateTime, module.EndTime.DateTime, TimeSpan.FromSeconds(1)); builder = new CountdownModuleBuilder() .WithMode(CountdownMode.Second) .WithEndTime(DateTimeOffset.Now.AddMinutes(1)) .WithStartTime(DateTimeOffset.Now.AddMinutes(-1)); + module = builder.Build(); Assert.Equal(DateTimeOffset.Now.AddMinutes(1).DateTime, builder.EndTime.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddMinutes(1).DateTime, builder.Build().EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.Equal(DateTimeOffset.Now.AddMinutes(1).DateTime, module.EndTime.DateTime, TimeSpan.FromSeconds(1)); + Assert.NotNull(builder.StartTime); Assert.Equal(DateTimeOffset.Now.AddMinutes(-1).DateTime, builder.StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); - Assert.Equal(DateTimeOffset.Now.AddMinutes(-1).DateTime, builder.Build().StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); + Assert.NotNull(module.StartTime); + Assert.Equal(DateTimeOffset.Now.AddMinutes(-1).DateTime, module.StartTime.Value.DateTime, TimeSpan.FromSeconds(1)); } [Fact] @@ -600,9 +619,6 @@ public void InviteModuleBuilder_Constructor() } [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] [InlineData("123456")] [InlineData("text")] [InlineData("hUsYbZ")] diff --git a/test/Kook.Net.Tests.Unit/TimeSpanTypeReaderTests.cs b/test/Kook.Net.Tests.Unit/TimeSpanTypeReaderTests.cs index 1533abf8..3d6f338b 100644 --- a/test/Kook.Net.Tests.Unit/TimeSpanTypeReaderTests.cs +++ b/test/Kook.Net.Tests.Unit/TimeSpanTypeReaderTests.cs @@ -43,29 +43,30 @@ public class TimeSpanTypeReaderTests public async Task TestTimeSpanParse(string input, bool isNegative) { TimeSpanTypeReader reader = new(); - TypeReaderResult result = await reader.ReadAsync(null, input, null); + TypeReaderResult result = await reader.ReadAsync(null!, input, null); Assert.True(result.IsSuccess); - TimeSpan actual = (TimeSpan)result.BestMatch; + TimeSpan? actual = (TimeSpan?)result.BestMatch; + Assert.NotNull(actual); Assert.True(actual != TimeSpan.Zero); if (isNegative) { Assert.True(actual < TimeSpan.Zero); - Assert.True(actual.Seconds == 0 || actual.Seconds == -1); - Assert.True(actual.Minutes == 0 || actual.Minutes == -2); - Assert.True(actual.Hours == 0 || actual.Hours == -3); - Assert.True(actual.Days == 0 || actual.Days == -4); + Assert.True(actual.Value.Seconds is 0 or -1); + Assert.True(actual.Value.Minutes is 0 or -2); + Assert.True(actual.Value.Hours is 0 or -3); + Assert.True(actual.Value.Days is 0 or -4); } else { Assert.True(actual > TimeSpan.Zero); - Assert.True(actual.Seconds == 0 || actual.Seconds == 1); - Assert.True(actual.Minutes == 0 || actual.Minutes == 2); - Assert.True(actual.Hours == 0 || actual.Hours == 3); - Assert.True(actual.Days == 0 || actual.Days == 4); + Assert.True(actual.Value.Seconds is 0 or 1); + Assert.True(actual.Value.Minutes is 0 or 2); + Assert.True(actual.Value.Hours is 0 or 3); + Assert.True(actual.Value.Days is 0 or 4); } } } diff --git a/test/Kook.Net.Tests.Unit/TokenUtilsTests.cs b/test/Kook.Net.Tests.Unit/TokenUtilsTests.cs index 00265dad..d07f2d98 100644 --- a/test/Kook.Net.Tests.Unit/TokenUtilsTests.cs +++ b/test/Kook.Net.Tests.Unit/TokenUtilsTests.cs @@ -20,11 +20,11 @@ public class TokenUtilsTests [InlineData(" ")] [InlineData(" ")] [InlineData("\t")] - public void NullOrWhitespaceToken(string token) + public void NullOrWhitespaceToken(string? token) { // an ArgumentNullException should be thrown, regardless of the TokenType foreach (TokenType tokenType in (TokenType[])Enum.GetValues(typeof(TokenType))) - Assert.Throws(() => TokenUtils.ValidateToken(tokenType, token)); + Assert.Throws(() => TokenUtils.ValidateToken(tokenType, token!)); } /// @@ -102,7 +102,8 @@ public void UnrecognizedTokenType(int type) => // should not throw an unexpected exception [InlineData("", false)] [InlineData(null, false)] - public void CheckBotTokenValidity(string token, bool expected) => Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token)); + public void CheckBotTokenValidity(string? token, bool expected) => + Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token!)); [Theory] // cannot pass a ulong? as a param in InlineData, so have to have a separate param @@ -114,9 +115,9 @@ public void UnrecognizedTokenType(int type) => [InlineData(" ", true, 0)] [InlineData(null, true, 0)] [InlineData("these chars aren't allowed @U#)*@#!)*", true, 0)] - public void DecodeBase64UserId(string encodedUserId, bool isNull, ulong expectedUserId) + public void DecodeBase64UserId(string? encodedUserId, bool isNull, ulong expectedUserId) { - ulong? result = TokenUtils.DecodeBase64UserId(encodedUserId); + ulong? result = TokenUtils.DecodeBase64UserId(encodedUserId!); if (isNull) Assert.Null(result); else diff --git a/test/Kook.Net.Tests.Unit/TypeReaderTests.cs b/test/Kook.Net.Tests.Unit/TypeReaderTests.cs index d7192139..0c40adc8 100644 --- a/test/Kook.Net.Tests.Unit/TypeReaderTests.cs +++ b/test/Kook.Net.Tests.Unit/TypeReaderTests.cs @@ -12,7 +12,7 @@ public sealed class TypeReaderTests public async Task TestNamedArgumentReader() { using CommandService commands = new(); - ModuleInfo module = await commands.AddModuleAsync(null); + ModuleInfo module = await commands.AddModuleAsync(null!); Assert.NotNull(module); Assert.NotEmpty(module.Commands); @@ -25,10 +25,10 @@ public async Task TestNamedArgumentReader() Assert.NotNull(param); Assert.True(param.IsRemainder); - TypeReaderResult result = await param.ParseAsync(null, "bar: hello foo: 42"); + TypeReaderResult result = await param.ParseAsync(null!, "bar: hello foo: 42"); Assert.True(result.IsSuccess); - ArgumentType m = result.BestMatch as ArgumentType; + ArgumentType? m = result.BestMatch as ArgumentType; Assert.NotNull(m); Assert.Equal(42, m.Foo); Assert.Equal("hello", m.Bar); @@ -38,7 +38,7 @@ public async Task TestNamedArgumentReader() public async Task TestQuotedArgumentValue() { using CommandService commands = new(); - ModuleInfo module = await commands.AddModuleAsync(null); + ModuleInfo module = await commands.AddModuleAsync(null!); Assert.NotNull(module); Assert.NotEmpty(module.Commands); @@ -51,10 +51,10 @@ public async Task TestQuotedArgumentValue() Assert.NotNull(param); Assert.True(param.IsRemainder); - TypeReaderResult result = await param.ParseAsync(null, "foo: 42 bar: 《hello》"); + TypeReaderResult result = await param.ParseAsync(null!, "foo: 42 bar: 《hello》"); Assert.True(result.IsSuccess); - ArgumentType m = result.BestMatch as ArgumentType; + ArgumentType? m = result.BestMatch as ArgumentType; Assert.NotNull(m); Assert.Equal(42, m.Foo); Assert.Equal("hello", m.Bar); @@ -64,7 +64,7 @@ public async Task TestQuotedArgumentValue() public async Task TestNonPatternInput() { using CommandService commands = new(); - ModuleInfo module = await commands.AddModuleAsync(null); + ModuleInfo module = await commands.AddModuleAsync(null!); Assert.NotNull(module); Assert.NotEmpty(module.Commands); @@ -77,7 +77,7 @@ public async Task TestNonPatternInput() Assert.NotNull(param); Assert.True(param.IsRemainder); - TypeReaderResult result = await param.ParseAsync(null, "foobar"); + TypeReaderResult result = await param.ParseAsync(null!, "foobar"); Assert.False(result.IsSuccess); Assert.Equal(CommandError.Exception, result.Error); } @@ -86,7 +86,7 @@ public async Task TestNonPatternInput() public async Task TestMultiple() { using CommandService commands = new(); - ModuleInfo module = await commands.AddModuleAsync(null); + ModuleInfo module = await commands.AddModuleAsync(null!); Assert.NotNull(module); Assert.NotEmpty(module.Commands); @@ -99,10 +99,10 @@ public async Task TestMultiple() Assert.NotNull(param); Assert.True(param.IsRemainder); - TypeReaderResult result = await param.ParseAsync(null, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); + TypeReaderResult result = await param.ParseAsync(null!, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); Assert.True(result.IsSuccess); - ArgumentType m = result.BestMatch as ArgumentType; + ArgumentType? m = result.BestMatch as ArgumentType; Assert.NotNull(m); Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7 }, m.ManyInts); } @@ -111,18 +111,18 @@ public async Task TestMultiple() [NamedArgumentType] public sealed class ArgumentType { - public int Foo { get; set; } + public required int Foo { get; set; } [OverrideTypeReader(typeof(CustomTypeReader))] - public string Bar { get; set; } + public required string Bar { get; set; } - public IEnumerable ManyInts { get; set; } + public required IEnumerable ManyInts { get; set; } } public sealed class CustomTypeReader : TypeReader { - public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) - => Task.FromResult(TypeReaderResult.FromSuccess(input)); + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) => + Task.FromResult(TypeReaderResult.FromSuccess(input)); } public sealed class TestModule : ModuleBase diff --git a/test/Kook.Net.Tests.Unit/UrlValidationTests.cs b/test/Kook.Net.Tests.Unit/UrlValidationTests.cs index 5e709365..b957a773 100644 --- a/test/Kook.Net.Tests.Unit/UrlValidationTests.cs +++ b/test/Kook.Net.Tests.Unit/UrlValidationTests.cs @@ -10,18 +10,31 @@ public class UrlValidationTests [Theory] [InlineData("http://kaiheila.net")] [InlineData("https://kaiheila.net")] - public void UrlValidation_ValidUrl(string url) => Assert.True(UrlValidation.Validate(url)); + public void UrlValidation_ValidUrl(string url) + { + try + { + UrlValidation.Validate(url); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + } [Theory] [InlineData(null)] [InlineData("")] - public void UrlValidation_EmptyUrl(string url) => Assert.False(UrlValidation.Validate(url)); + public void UrlValidation_EmptyUrl(string? url) => + Assert.Throws(() => UrlValidation.Validate(url!)); + [Theory] [InlineData(" ")] [InlineData("kaiheila.net")] [InlineData("steam://run/123456/")] - public void UrlValidation_InvalidUrl(string url) => Assert.Throws(() => UrlValidation.Validate(url)); + public void UrlValidation_InvalidUrl(string url) => + Assert.Throws(() => UrlValidation.Validate(url)); [Theory] [InlineData("http://img.kaiheila.cn/assets/2021-01/7kr4FkWpLV0ku0ku.jpeg")] @@ -33,7 +46,7 @@ public class UrlValidationTests [Theory] [InlineData(null)] [InlineData("")] - public void UrlValidation_EmptyAssetUrl(string url) => Assert.False(UrlValidation.ValidateKookAssetUrl(url)); + public void UrlValidation_EmptyAssetUrl(string? url) => Assert.False(UrlValidation.ValidateKookAssetUrl(url!)); [Theory] [InlineData(" ")]