From 5bdd6a7ff3cf2971bcd17ef853ffb22adecd377e Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 4 Apr 2016 20:15:16 -0300 Subject: [PATCH] Early 1.0 REST Preview --- Discord.Net.sln | 102 +- NuGet.config | 8 + docs/features/commands.rst | 2 +- docs/features/events.rst | 20 +- docs/features/logging.rst | 16 +- docs/features/permissions.rst | 40 +- docs/features/server-management.rst | 2 +- docs/features/user-management.rst | 4 +- docs/features/voice.rst | 175 +-- docs/getting_started.rst | 6 +- docs/global.txt | 4 +- docs/index.rst | 12 +- docs/samples/events.cs | 14 +- docs/samples/getting_started.cs | 9 +- docs/samples/logging.cs | 5 +- docs/samples/permissions.cs | 16 +- global.json | 8 +- ref/Discord.Net.xproj | 21 - ref/DiscordClient.cs | 75 - ref/DiscordConfig.cs | 65 - ref/Entities/Channels/IPublicChannel.cs | 43 - ref/Entities/Channels/PrivateChannel.cs | 55 - ref/Entities/Channels/TextChannel.cs | 91 -- ref/Entities/Channels/VoiceChannel.cs | 74 - ref/Entities/Color.cs | 17 - ref/Entities/IModifiable.cs | 11 - ref/Entities/Invite/BasicInvite.cs | 37 - ref/Entities/Invite/Invite.cs | 18 - ref/Entities/Message.cs | 68 - .../Permissions/ChannelPermissions.cs | 53 - .../Permissions/OverwritePermissions.cs | 50 - .../Permissions/PermissionOverwriteEntry.cs | 9 - ref/Entities/Permissions/ServerPermissions.cs | 55 - ref/Entities/Profile.cs | 25 - ref/Entities/Region.cs | 11 - ref/Entities/Role.cs | 29 - ref/Entities/Server.cs | 67 - ref/Entities/Users/IUser.cs | 19 - ref/Entities/Users/PrivateUser.cs | 43 - ref/Entities/Users/ServerUser.cs | 73 - ref/Enums/ChannelType.cs | 13 - ref/Enums/ConnectionState.cs | 10 - ref/Enums/EntityState.cs | 18 - ref/Enums/ImageType.cs | 9 - ref/Enums/LogSeverity.cs | 12 - ref/Enums/PermValue.cs | 9 - ref/Enums/PermissionTarget.cs | 8 - ref/Enums/Relative.cs | 8 - ref/Enums/UserStatus.cs | 9 - ref/Events/ChannelEventArgs.cs | 9 - ref/Events/ChannelUpdatedEventArgs.cs | 10 - ref/Events/DisconnectedEventArgs.cs | 10 - ref/Events/LogMessageEventArgs.cs | 12 - ref/Events/MessageEventArgs.cs | 11 - ref/Events/MessageUpdatedEventArgs.cs | 12 - ref/Events/ProfileUpdatedEventArgs.cs | 10 - ref/Events/RoleEventArgs.cs | 10 - ref/Events/RoleUpdatedEventArgs.cs | 11 - ref/Events/ServerEventArgs.cs | 9 - ref/Events/ServerUpdatedEventArgs.cs | 10 - ref/Events/TypingEventArgs.cs | 14 - ref/Events/UserEventArgs.cs | 8 - ref/Events/UserUpdatedEventArgs.cs | 9 - ref/Format.cs | 14 - ref/ILogger.cs | 30 - ref/MessageQueue.cs | 9 - ref/Net/HttpException.cs | 16 - ref/Net/Rest/CompletedRequestEventArgs.cs | 17 - ref/Net/Rest/IRestClient.cs | 23 - ref/Net/Rest/IRestClientProvider.cs | 10 - ref/Net/Rest/RequestEventArgs.cs | 12 - ref/Net/TimeoutException.cs | 9 - ref/Net/WebSocketException.cs | 12 - ref/Net/WebSockets/BinaryMessageEventArgs.cs | 11 - ref/Net/WebSockets/GatewaySocket.cs | 28 - ref/Net/WebSockets/IWebSocket.cs | 15 - ref/Net/WebSockets/IWebSocketEngine.cs | 18 - ref/Net/WebSockets/IWebSocketProvider.cs | 9 - ref/Net/WebSockets/TextMessageEventArgs.cs | 11 - ref/Net/WebSockets/WebSocketEventEventArgs.cs | 11 - ref/project.json | 81 -- src/Discord.Net.Audio/AudioClient.cs | 283 ---- src/Discord.Net.Audio/AudioExtensions.cs | 26 - src/Discord.Net.Audio/AudioMode.cs | 9 - src/Discord.Net.Audio/AudioService.cs | 193 --- src/Discord.Net.Audio/AudioServiceConfig.cs | 51 - src/Discord.Net.Audio/Discord.Net.Audio.xproj | 21 - src/Discord.Net.Audio/IAudioClient.cs | 46 - .../InternalFrameEventArgs.cs | 22 - .../InternalIsSpeakingEventArgs.cs | 14 - src/Discord.Net.Audio/Net/VoiceSocket.cs | 516 ------- src/Discord.Net.Audio/Opus/OpusConverter.cs | 111 -- src/Discord.Net.Audio/Opus/OpusDecoder.cs | 43 - src/Discord.Net.Audio/Opus/OpusEncoder.cs | 78 -- src/Discord.Net.Audio/Sodium/SecretBox.cs | 32 - .../UserIsTalkingEventArgs.cs | 13 - src/Discord.Net.Audio/VirtualClient.cs | 39 - src/Discord.Net.Audio/VoiceBuffer.cs | 140 -- .../VoiceDisconnectedEventArgs.cs | 15 - src/Discord.Net.Audio/libsodium.dll | Bin 492032 -> 0 bytes src/Discord.Net.Audio/opus.dll | Bin 271872 -> 0 bytes src/Discord.Net.Audio/project.json | 27 - src/Discord.Net.Commands/Command.cs | 83 -- src/Discord.Net.Commands/CommandBuilder.cs | 163 --- .../CommandErrorEventArgs.cs | 18 - src/Discord.Net.Commands/CommandEventArgs.cs | 26 - src/Discord.Net.Commands/CommandExtensions.cs | 20 - src/Discord.Net.Commands/CommandMap.cs | 141 -- src/Discord.Net.Commands/CommandParameter.cs | 26 - src/Discord.Net.Commands/CommandParser.cs | 188 --- src/Discord.Net.Commands/CommandService.cs | 347 ----- .../CommandServiceConfig.cs | 46 - .../Discord.Net.Commands.xproj | 21 - .../GenericPermissionChecker.cs | 22 - src/Discord.Net.Commands/HelpMode.cs | 12 - .../IPermissionChecker.cs | 7 - src/Discord.Net.Commands/project.json | 25 - .../Discord.Net.Modules.xproj | 21 - src/Discord.Net.Modules/IModule.cs | 7 - src/Discord.Net.Modules/ModuleChecker.cs | 33 - src/Discord.Net.Modules/ModuleExtensions.cs | 29 - src/Discord.Net.Modules/ModuleFilter.cs | 17 - src/Discord.Net.Modules/ModuleManager.cs | 278 ---- src/Discord.Net.Modules/ModuleService.cs | 61 - src/Discord.Net.Modules/project.json | 26 - .../Discord.Net.Shared.projitems | 16 - .../Discord.Net.Shared.shproj | 13 - src/Discord.Net.Shared/EpochTime.cs | 11 - src/Discord.Net.Shared/TaskExtensions.cs | 68 - src/Discord.Net.Shared/TaskHelper.cs | 29 - src/Discord.Net/API/Client/Common/Channel.cs | 35 - .../API/Client/Common/ChannelReference.cs | 17 - .../API/Client/Common/ExtendedMember.cs | 12 - src/Discord.Net/API/Client/Common/Guild.cs | 50 - .../API/Client/Common/GuildReference.cs | 13 - src/Discord.Net/API/Client/Common/Invite.cs | 23 - .../API/Client/Common/InviteReference.cs | 22 - src/Discord.Net/API/Client/Common/Member.cs | 14 - .../API/Client/Common/MemberPresence.cs | 20 - .../API/Client/Common/MemberReference.cs | 13 - src/Discord.Net/API/Client/Common/Message.cs | 96 -- .../API/Client/Common/MessageReference.cs | 15 - .../API/Client/Common/RoleReference.cs | 13 - src/Discord.Net/API/Client/Common/User.cs | 12 - .../API/Client/Common/UserReference.cs | 17 - .../GatewaySocket/Commands/Heartbeat.cs | 12 - .../GatewaySocket/Events/GuildEmojisUpdate.cs | 4 - .../Events/GuildIntegrationsUpdate.cs | 4 - .../GatewaySocket/Events/GuildMemberRemove.cs | 4 - .../GatewaySocket/Events/GuildMemberUpdate.cs | 4 - .../GatewaySocket/Events/GuildRoleCreate.cs | 13 - .../GatewaySocket/Events/GuildRoleUpdate.cs | 13 - .../GatewaySocket/Events/TypingStart.cs | 15 - .../Events/UserSettingsUpdate.cs | 4 - .../GatewaySocket/Events/VoiceServerUpdate.cs | 15 - src/Discord.Net/API/Client/ISerializable.cs | 9 - .../API/Client/Rest/AcceptInvite.cs | 19 - .../API/Client/Rest/AddGuildBan.cs | 23 - src/Discord.Net/API/Client/Rest/DeleteRole.cs | 21 - src/Discord.Net/API/Client/Rest/Gateway.cs | 18 - src/Discord.Net/API/Client/Rest/GetBans.cs | 19 - src/Discord.Net/API/Client/Rest/GetInvite.cs | 19 - src/Discord.Net/API/Client/Rest/GetInvites.cs | 19 - .../API/Client/Rest/GetMessages.cs | 34 - src/Discord.Net/API/Client/Rest/GetWidget.cs | 60 - src/Discord.Net/API/Client/Rest/KickMember.cs | 21 - src/Discord.Net/API/Client/Rest/Login.cs | 23 - src/Discord.Net/API/Client/Rest/Logout.cs | 12 - .../API/Client/Rest/PruneMembers.cs | 28 - .../Client/Rest/RemoveChannelPermission.cs | 21 - .../API/Client/Rest/ReorderChannels.cs | 45 - .../API/Client/Rest/ReorderRoles.cs | 45 - .../API/Client/Rest/SendIsTyping.cs | 19 - .../API/Client/Rest/UpdateGuild.cs | 33 - src/Discord.Net/API/Client/Rest/UpdateRole.cs | 30 - .../Client/VoiceSocket/Commands/Heartbeat.cs | 9 - .../Client/VoiceSocket/Commands/Identify.cs | 21 - .../VoiceSocket/Commands/SelectProtocol.cs | 29 - src/Discord.Net/API/Common/Attachment.cs | 22 + src/Discord.Net/API/Common/Channel.cs | 37 + src/Discord.Net/API/Common/Embed.cs | 21 + src/Discord.Net/API/Common/EmbedProvider.cs | 12 + src/Discord.Net/API/Common/EmbedThumbnail.cs | 16 + src/Discord.Net/API/Common/Emoji.cs | 18 + src/Discord.Net/API/Common/Guild.cs | 36 + src/Discord.Net/API/Common/GuildEmbed.cs | 12 + src/Discord.Net/API/Common/GuildMember.cs | 19 + src/Discord.Net/API/Common/Integration.cs | 32 + .../API/Common/IntegrationAccount.cs | 12 + src/Discord.Net/API/Common/Invite.cs | 16 + src/Discord.Net/API/Common/InviteChannel.cs | 15 + src/Discord.Net/API/Common/InviteGuild.cs | 14 + src/Discord.Net/API/Common/InviteMetadata.cs | 23 + src/Discord.Net/API/Common/Message.cs | 31 + src/Discord.Net/API/Common/ReadState.cs | 14 + .../API/Common/Unconfirmed/Connection.cs | 19 + .../Unconfirmed}/ExtendedGuild.cs | 2 +- .../API/Common/Unconfirmed/ExtendedMember.cs | 12 + .../API/Common/Unconfirmed/MemberPresence.cs | 15 + .../Common/Unconfirmed/MemberPresenceGame.cs | 10 + .../API/Common/Unconfirmed/MemberReference.cs | 12 + .../Unconfirmed}/MemberVoiceState.cs | 17 +- .../Common/Unconfirmed/MessageReference.cs | 14 + .../API/Common/Unconfirmed/Overwrite.cs | 16 + .../Common => Common/Unconfirmed}/Role.cs | 7 +- .../API/Common/Unconfirmed/RoleReference.cs | 12 + src/Discord.Net/API/Common/User.cs | 22 + src/Discord.Net/API/Common/UserGuild.cs | 16 + src/Discord.Net/API/Converters.cs | 89 -- src/Discord.Net/API/Extensions.cs | 19 - .../OpCodes.cs => GatewaySocket/OpCode.cs} | 14 +- .../Unconfirmed/Commands/Heartbeat.cs | 12 + .../Unconfirmed}/Commands/Identify.cs | 7 +- .../Unconfirmed}/Commands/RequestMembers.cs | 10 +- .../Unconfirmed}/Commands/Resume.cs | 5 +- .../Unconfirmed}/Commands/UpdateStatus.cs | 5 +- .../Unconfirmed}/Commands/UpdateVoice.cs | 12 +- .../Unconfirmed}/Events/ChannelCreate.cs | 2 +- .../Unconfirmed}/Events/ChannelDelete.cs | 2 +- .../Unconfirmed}/Events/ChannelUpdate.cs | 2 +- .../Unconfirmed}/Events/GuildBanAdd.cs | 2 +- .../Unconfirmed}/Events/GuildBanRemove.cs | 2 +- .../Unconfirmed}/Events/GuildCreate.cs | 2 +- .../Unconfirmed}/Events/GuildDelete.cs | 2 +- .../Unconfirmed}/Events/GuildMemberAdd.cs | 2 +- .../Unconfirmed/Events/GuildMemberRemove.cs | 4 + .../Unconfirmed/Events/GuildMemberUpdate.cs | 4 + .../Unconfirmed}/Events/GuildMembersChunk.cs | 7 +- .../Unconfirmed/Events/GuildRoleCreate.cs | 12 + .../Unconfirmed}/Events/GuildRoleDelete.cs | 2 +- .../Unconfirmed/Events/GuildRoleUpdate.cs | 12 + .../Unconfirmed}/Events/GuildUpdate.cs | 2 +- .../Unconfirmed}/Events/MessageAck.cs | 2 +- .../Unconfirmed}/Events/MessageCreate.cs | 2 +- .../Unconfirmed}/Events/MessageDelete.cs | 2 +- .../Unconfirmed}/Events/MessageUpdate.cs | 2 +- .../Unconfirmed}/Events/PresenceUpdate.cs | 2 +- .../Unconfirmed}/Events/Ready.cs | 2 +- .../Unconfirmed}/Events/Redirect.cs | 2 +- .../Unconfirmed}/Events/Resumed.cs | 2 +- .../Unconfirmed/Events/TypingStart.cs | 14 + .../Unconfirmed}/Events/UserUpdate.cs | 2 +- .../Unconfirmed/Events/VoiceServerUpdate.cs | 14 + .../Unconfirmed}/Events/VoiceStateUpdate.cs | 2 +- src/Discord.Net/API/IRestRequest.cs | 25 - .../API/{Client => }/IWebSocketMessage.cs | 3 +- src/Discord.Net/API/Rest/AcceptInvite.cs | 18 + .../API/{Client => }/Rest/AckMessage.cs | 12 +- src/Discord.Net/API/Rest/BeginGuildPrune.cs | 23 + .../CreateChannelInvite.cs} | 15 +- .../CreateDMChannel.cs} | 8 +- .../API/{Client => }/Rest/CreateGuild.cs | 11 +- src/Discord.Net/API/Rest/CreateGuildBan.cs | 24 + .../CreateGuildChannel.cs} | 9 +- .../API/Rest/CreateGuildIntegration.cs | 25 + .../CreateRole.cs => Rest/CreateGuildRole.cs} | 7 +- .../SendMessage.cs => Rest/CreateMessage.cs} | 17 +- .../API/{Client => }/Rest/DeleteChannel.cs | 7 +- .../API/Rest/DeleteChannelPermission.cs | 20 + .../API/{Client => }/Rest/DeleteGuild.cs | 7 +- .../API/Rest/DeleteGuildIntegration.cs | 20 + src/Discord.Net/API/Rest/DeleteGuildRole.cs | 20 + .../API/{Client => }/Rest/DeleteInvite.cs | 9 +- .../API/{Client => }/Rest/DeleteMessage.cs | 9 +- src/Discord.Net/API/Rest/GetChannel.cs | 18 + src/Discord.Net/API/Rest/GetChannelInvites.cs | 20 + .../API/Rest/GetChannelMessages.cs | 34 + src/Discord.Net/API/Rest/GetCurrentUser.cs | 11 + .../API/Rest/GetCurrentUserConnections.cs | 11 + src/Discord.Net/API/Rest/GetCurrentUserDMs.cs | 11 + .../API/Rest/GetCurrentUserGuilds.cs | 11 + src/Discord.Net/API/Rest/GetGateway.cs | 18 + src/Discord.Net/API/Rest/GetGuild.cs | 18 + src/Discord.Net/API/Rest/GetGuildBans.cs | 18 + src/Discord.Net/API/Rest/GetGuildChannels.cs | 18 + src/Discord.Net/API/Rest/GetGuildEmbed.cs | 18 + .../API/Rest/GetGuildIntegrations.cs | 18 + src/Discord.Net/API/Rest/GetGuildInvites.cs | 18 + src/Discord.Net/API/Rest/GetGuildMember.cs | 20 + .../API/Rest/GetGuildPruneCount.cs | 29 + src/Discord.Net/API/Rest/GetGuildRoles.cs | 18 + .../API/Rest/GetGuildVoiceRegions.cs | 18 + src/Discord.Net/API/Rest/GetInvite.cs | 18 + src/Discord.Net/API/Rest/GetUser.cs | 18 + .../API/{Client => }/Rest/GetVoiceRegions.cs | 18 +- .../API/{Client => }/Rest/LeaveGuild.cs | 7 +- src/Discord.Net/API/Rest/ListGuildMembers.cs | 24 + .../ModifyChannelPermission.cs} | 16 +- .../ModifyCurrentUser.cs} | 23 +- src/Discord.Net/API/Rest/ModifyGuild.cs | 39 + .../ModifyGuildChannel.cs} | 13 +- .../API/Rest/ModifyGuildChannels.cs | 22 + src/Discord.Net/API/Rest/ModifyGuildEmbed.cs | 25 + .../API/Rest/ModifyGuildIntegration.cs | 29 + .../ModifyGuildMember.cs} | 20 +- src/Discord.Net/API/Rest/ModifyGuildRole.cs | 32 + src/Discord.Net/API/Rest/ModifyGuildRoles.cs | 21 + .../ModifyMessage.cs} | 9 +- src/Discord.Net/API/Rest/ModifyTextChannel.cs | 16 + .../API/Rest/ModifyVoiceChannel.cs | 16 + src/Discord.Net/API/Rest/QueryUser.cs | 19 + .../API/{Client => }/Rest/RemoveGuildBan.cs | 9 +- src/Discord.Net/API/Rest/RemoveGuildMember.cs | 20 + .../API/Rest/SyncGuildIntegration.cs | 21 + .../API/Rest/TriggerTypingIndicator.cs | 18 + .../Rest => Rest/Unconfirmed}/SendFile.cs | 18 +- .../API/Status/Common/StatusResult.cs | 80 -- .../API/Status/Rest/ActiveMaintenances.cs | 12 - .../API/Status/Rest/AllIncidents.cs | 12 - .../API/Status/Rest/UnresolvedIncidents.cs | 12 - .../API/Status/Rest/UpcomingMaintenances.cs | 12 - .../OpCodes.cs => VoiceSocket/OpCode.cs} | 6 +- .../Unconfirmed/Commands/Heartbeat.cs | 10 + .../Unconfirmed/Commands/Identify.cs | 19 + .../Unconfirmed/Commands/SelectProtocol.cs | 28 + .../Unconfirmed}/Commands/SetSpeaking.cs | 5 +- .../Unconfirmed}/Events/Ready.cs | 2 +- .../Unconfirmed}/Events/SessionDescription.cs | 2 +- .../Unconfirmed}/Events/Speaking.cs | 7 +- src/Discord.Net/CDN.cs | 12 + src/Discord.Net/Discord.Net.Net45.csproj | 305 ++++ .../Discord.Net.Net45.project.json | 12 + .../Discord.Net.Net45.project.lock.json | 88 ++ src/Discord.Net/Discord.Net.xproj | 12 +- src/Discord.Net/DiscordClient.Events.cs | 108 -- src/Discord.Net/DiscordClient.cs | 1227 ++++------------- src/Discord.Net/DiscordConfig.cs | 142 +- src/Discord.Net/DiscordSocketClient.cs | 176 +++ src/Discord.Net/DynamicIL.cs | 49 - src/Discord.Net/ETF/ETFReader.cs | 491 ------- src/Discord.Net/ETF/ETFType.cs | 32 - src/Discord.Net/ETF/ETFWriter.cs | 482 ------- src/Discord.Net/Entities/Attachment.cs | 18 + src/Discord.Net/Entities/Channel.cs | 55 - .../Entities/Channels/DMChannel.cs | 99 ++ .../Entities/Channels/GuildChannel.cs | 130 ++ .../Entities/Channels/IChannel.cs | 7 +- .../Entities/Channels/IMessageChannel.cs | 4 +- .../Entities/Channels/TextChannel.cs | 67 + .../Entities/Channels/VoiceChannel.cs | 39 + src/Discord.Net/Entities/Color.cs | 41 +- src/Discord.Net/Entities/Embed.cs | 25 + src/Discord.Net/Entities/EmbedProvider.cs | 16 + src/Discord.Net/Entities/EmbedThumbnail.cs | 20 + src/Discord.Net/Entities/Emoji.cs | 28 + src/Discord.Net/Entities/Guild.cs | 383 +++++ .../Entities/Helpers/InviteManager.cs | 8 + .../Entities/Helpers/MentionHelper.cs | 111 ++ .../Entities/Helpers/MessageManager.cs | 174 +++ .../Entities/Helpers/PermissionManager.cs | 274 ++++ .../Entities/Helpers/PermissionsHelper.cs | 61 + src/Discord.Net/Entities/IChannel.cs | 21 - {ref => src/Discord.Net}/Entities/IEntity.cs | 5 +- .../Discord.Net}/Entities/IMentionable.cs | 0 src/Discord.Net/Entities/IPrivateChannel.cs | 7 - src/Discord.Net/Entities/IPublicChannel.cs | 6 - src/Discord.Net/Entities/ITextChannel.cs | 17 - src/Discord.Net/Entities/IVoiceChannel.cs | 6 - src/Discord.Net/Entities/Invite.cs | 151 -- .../Entities/Invites/GuildInvite.cs | 71 + src/Discord.Net/Entities/Invites/IInvite.cs | 20 + src/Discord.Net/Entities/Invites/Invite.cs | 48 + .../Entities/Invites/InviteChannel.cs | 16 + .../Entities/Invites/InviteGuild.cs | 16 + .../Entities/Managers/MessageManager.cs | 146 -- .../Entities/Managers/PermissionManager.cs | 223 --- src/Discord.Net/Entities/Message.cs | 375 ++--- src/Discord.Net/Entities/Permissions.cs | 345 ----- .../Permissions/ChannelPermissions.cs | 122 ++ .../Entities/Permissions/GuildPermissions.cs | 119 ++ .../Entities/Permissions/Overwrite.cs | 22 + .../Permissions/OverwritePermissions.cs | 113 ++ .../Entities/Presences/GuildPresence.cs | 33 + .../Entities/Presences/Presence.cs | 18 + .../Entities/Presences/VoiceState.cs | 66 + src/Discord.Net/Entities/PrivateChannel.cs | 66 - src/Discord.Net/Entities/Profile.cs | 92 -- src/Discord.Net/Entities/PublicChannel.cs | 109 -- src/Discord.Net/Entities/Region.cs | 22 - src/Discord.Net/Entities/Role.cs | 143 +- src/Discord.Net/Entities/Server.cs | 511 ------- src/Discord.Net/Entities/TextChannel.cs | 88 -- src/Discord.Net/Entities/User.cs | 356 ----- src/Discord.Net/Entities/Users/DMUser.cs | 15 + src/Discord.Net/Entities/Users/GuildUser.cs | 48 + src/Discord.Net/Entities/Users/PublicUser.cs | 13 + src/Discord.Net/Entities/Users/SelfUser.cs | 26 + src/Discord.Net/Entities/Users/User.cs | 39 + src/Discord.Net/Entities/VoiceChannel.cs | 60 - src/Discord.Net/Entities/VoiceRegion.cs | 60 + src/Discord.Net/EnumConverters.cs | 42 - src/Discord.Net/Enums/ChannelType.cs | 14 +- src/Discord.Net/Enums/ConnectionState.cs | 2 +- src/Discord.Net/Enums/ImageType.cs | 9 - src/Discord.Net/Enums/LogSeverity.cs | 3 +- src/Discord.Net/Enums/MessageState.cs | 18 - src/Discord.Net/Enums/PermissionBits.cs | 4 +- src/Discord.Net/Enums/PermissionTarget.cs | 8 +- src/Discord.Net/Enums/Relative.cs | 3 +- src/Discord.Net/Enums/StringEnum.cs | 14 - src/Discord.Net/Enums/UserStatus.cs | 41 +- src/Discord.Net/Events/ChannelEventArgs.cs | 5 +- .../Events/ChannelUpdatedEventArgs.cs | 10 +- .../Events/CurrentUserEventArgs.cs | 14 + .../Events/CurrentUserUpdatedEventArgs.cs | 14 + .../Events/DisconnectedEventArgs.cs | 4 +- src/Discord.Net/Events/GuildEventArgs.cs | 14 + .../Events/GuildUpdatedEventArgs.cs | 14 + src/Discord.Net/Events/LogMessageEventArgs.cs | 47 +- src/Discord.Net/Events/MessageEventArgs.cs | 9 +- .../Events/MessageUpdatedEventArgs.cs | 14 +- .../Events/ProfileUpdatedEventArgs.cs | 16 - src/Discord.Net/Events/RoleEventArgs.cs | 7 +- .../Events/RoleUpdatedEventArgs.cs | 12 +- src/Discord.Net/Events/ServerEventArgs.cs | 11 - .../Events/ServerUpdatedEventArgs.cs | 16 - src/Discord.Net/Events/TypingEventArgs.cs | 10 +- src/Discord.Net/Events/UserEventArgs.cs | 8 +- .../Events/UserUpdatedEventArgs.cs | 11 +- .../Events/VoiceChannelEventArgs.cs | 14 + src/Discord.Net/Format.cs | 189 ++- src/Discord.Net/IMentionable.cs | 7 - src/Discord.Net/IModel.cs | 11 - src/Discord.Net/IService.cs | 7 - src/Discord.Net/InternalExtensions.cs | 77 +- src/Discord.Net/Logging/ILogger.cs | 44 - src/Discord.Net/Logging/LogManager.cs | 51 +- src/Discord.Net/Logging/Logger.cs | 34 +- src/Discord.Net/MessageQueue.cs | 256 ++-- src/Discord.Net/Net/HttpException.cs | 26 +- .../JsonConverters/ChannelTypeConverter.cs | 40 + .../Net/JsonConverters/ImageConverter.cs | 29 + .../JsonConverters/NullableUInt64Converter.cs | 30 + .../PermissionTargetConverter.cs | 40 + .../JsonConverters/StringEntityConverter.cs | 25 + .../JsonConverters/UInt64ArrayConverter.cs | 43 + .../Net/JsonConverters/UInt64Converter.cs | 23 + .../JsonConverters/UInt64EntityConverter.cs | 25 + .../Net/JsonConverters/UserStatusConverter.cs | 45 + src/Discord.Net/Net/Rest/BuiltInEngine.cs | 145 -- .../Net/Rest/CompletedRequestEventArgs.cs | 19 - src/Discord.Net/Net/Rest/DefaultRestEngine.cs | 127 ++ src/Discord.Net/Net/Rest/ETFRestClient.cs | 27 - src/Discord.Net/Net/Rest/IRestEngine.cs | 17 +- .../Discord.Net}/Net/Rest/IRestRequest.cs | 12 +- src/Discord.Net/Net/Rest/JsonRestClient.cs | 42 - src/Discord.Net/Net/Rest/RequestEventArgs.cs | 16 - src/Discord.Net/Net/Rest/RestClient.cs | 166 +-- .../Net/Rest/RestClientProvider.cs | 6 + src/Discord.Net/Net/Rest/RestParameter.cs | 19 + .../Net/Rest/SentRequestEventArgs.cs | 16 + src/Discord.Net/Net/Rest/SharpRestEngine.cs | 132 -- src/Discord.Net/Net/TimeoutException.cs | 15 - src/Discord.Net/Net/WebSocketException.cs | 25 - .../Net/WebSockets/BinaryMessageEventArgs.cs | 2 +- ...tInEngine.cs => DefaultWebSocketEngine.cs} | 106 +- .../Net/WebSockets/GatewaySocket.cs | 188 +-- .../Net/WebSockets/IWebSocketEngine.cs | 20 +- .../Net/WebSockets/WS4NetEngine.cs | 141 -- src/Discord.Net/Net/WebSockets/WebSocket.cs | 193 +-- .../Net/WebSockets/WebSocketEventEventArgs.cs | 6 - .../Net/WebSockets/WebSocketProvider.cs | 6 + src/Discord.Net/Properties/AssemblyInfo.cs | 18 + src/Discord.Net/ServiceCollection.cs | 44 - src/Discord.Net/TaskHelper.cs | 18 + src/Discord.Net/TaskManager.cs | 179 --- src/Discord.Net/project.json | 76 +- .../Discord.Net.Tests.csproj | 15 +- test/Discord.Net.Tests/Settings.cs | 32 - test/Discord.Net.Tests/Tests.cs | 641 ++++++--- 470 files changed, 6774 insertions(+), 14012 deletions(-) create mode 100644 NuGet.config delete mode 100644 ref/Discord.Net.xproj delete mode 100644 ref/DiscordClient.cs delete mode 100644 ref/DiscordConfig.cs delete mode 100644 ref/Entities/Channels/IPublicChannel.cs delete mode 100644 ref/Entities/Channels/PrivateChannel.cs delete mode 100644 ref/Entities/Channels/TextChannel.cs delete mode 100644 ref/Entities/Channels/VoiceChannel.cs delete mode 100644 ref/Entities/Color.cs delete mode 100644 ref/Entities/IModifiable.cs delete mode 100644 ref/Entities/Invite/BasicInvite.cs delete mode 100644 ref/Entities/Invite/Invite.cs delete mode 100644 ref/Entities/Message.cs delete mode 100644 ref/Entities/Permissions/ChannelPermissions.cs delete mode 100644 ref/Entities/Permissions/OverwritePermissions.cs delete mode 100644 ref/Entities/Permissions/PermissionOverwriteEntry.cs delete mode 100644 ref/Entities/Permissions/ServerPermissions.cs delete mode 100644 ref/Entities/Profile.cs delete mode 100644 ref/Entities/Region.cs delete mode 100644 ref/Entities/Role.cs delete mode 100644 ref/Entities/Server.cs delete mode 100644 ref/Entities/Users/IUser.cs delete mode 100644 ref/Entities/Users/PrivateUser.cs delete mode 100644 ref/Entities/Users/ServerUser.cs delete mode 100644 ref/Enums/ChannelType.cs delete mode 100644 ref/Enums/ConnectionState.cs delete mode 100644 ref/Enums/EntityState.cs delete mode 100644 ref/Enums/ImageType.cs delete mode 100644 ref/Enums/LogSeverity.cs delete mode 100644 ref/Enums/PermValue.cs delete mode 100644 ref/Enums/PermissionTarget.cs delete mode 100644 ref/Enums/Relative.cs delete mode 100644 ref/Enums/UserStatus.cs delete mode 100644 ref/Events/ChannelEventArgs.cs delete mode 100644 ref/Events/ChannelUpdatedEventArgs.cs delete mode 100644 ref/Events/DisconnectedEventArgs.cs delete mode 100644 ref/Events/LogMessageEventArgs.cs delete mode 100644 ref/Events/MessageEventArgs.cs delete mode 100644 ref/Events/MessageUpdatedEventArgs.cs delete mode 100644 ref/Events/ProfileUpdatedEventArgs.cs delete mode 100644 ref/Events/RoleEventArgs.cs delete mode 100644 ref/Events/RoleUpdatedEventArgs.cs delete mode 100644 ref/Events/ServerEventArgs.cs delete mode 100644 ref/Events/ServerUpdatedEventArgs.cs delete mode 100644 ref/Events/TypingEventArgs.cs delete mode 100644 ref/Events/UserEventArgs.cs delete mode 100644 ref/Events/UserUpdatedEventArgs.cs delete mode 100644 ref/Format.cs delete mode 100644 ref/ILogger.cs delete mode 100644 ref/MessageQueue.cs delete mode 100644 ref/Net/HttpException.cs delete mode 100644 ref/Net/Rest/CompletedRequestEventArgs.cs delete mode 100644 ref/Net/Rest/IRestClient.cs delete mode 100644 ref/Net/Rest/IRestClientProvider.cs delete mode 100644 ref/Net/Rest/RequestEventArgs.cs delete mode 100644 ref/Net/TimeoutException.cs delete mode 100644 ref/Net/WebSocketException.cs delete mode 100644 ref/Net/WebSockets/BinaryMessageEventArgs.cs delete mode 100644 ref/Net/WebSockets/GatewaySocket.cs delete mode 100644 ref/Net/WebSockets/IWebSocket.cs delete mode 100644 ref/Net/WebSockets/IWebSocketEngine.cs delete mode 100644 ref/Net/WebSockets/IWebSocketProvider.cs delete mode 100644 ref/Net/WebSockets/TextMessageEventArgs.cs delete mode 100644 ref/Net/WebSockets/WebSocketEventEventArgs.cs delete mode 100644 ref/project.json delete mode 100644 src/Discord.Net.Audio/AudioClient.cs delete mode 100644 src/Discord.Net.Audio/AudioExtensions.cs delete mode 100644 src/Discord.Net.Audio/AudioMode.cs delete mode 100644 src/Discord.Net.Audio/AudioService.cs delete mode 100644 src/Discord.Net.Audio/AudioServiceConfig.cs delete mode 100644 src/Discord.Net.Audio/Discord.Net.Audio.xproj delete mode 100644 src/Discord.Net.Audio/IAudioClient.cs delete mode 100644 src/Discord.Net.Audio/InternalFrameEventArgs.cs delete mode 100644 src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs delete mode 100644 src/Discord.Net.Audio/Net/VoiceSocket.cs delete mode 100644 src/Discord.Net.Audio/Opus/OpusConverter.cs delete mode 100644 src/Discord.Net.Audio/Opus/OpusDecoder.cs delete mode 100644 src/Discord.Net.Audio/Opus/OpusEncoder.cs delete mode 100644 src/Discord.Net.Audio/Sodium/SecretBox.cs delete mode 100644 src/Discord.Net.Audio/UserIsTalkingEventArgs.cs delete mode 100644 src/Discord.Net.Audio/VirtualClient.cs delete mode 100644 src/Discord.Net.Audio/VoiceBuffer.cs delete mode 100644 src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs delete mode 100644 src/Discord.Net.Audio/libsodium.dll delete mode 100644 src/Discord.Net.Audio/opus.dll delete mode 100644 src/Discord.Net.Audio/project.json delete mode 100644 src/Discord.Net.Commands/Command.cs delete mode 100644 src/Discord.Net.Commands/CommandBuilder.cs delete mode 100644 src/Discord.Net.Commands/CommandErrorEventArgs.cs delete mode 100644 src/Discord.Net.Commands/CommandEventArgs.cs delete mode 100644 src/Discord.Net.Commands/CommandExtensions.cs delete mode 100644 src/Discord.Net.Commands/CommandMap.cs delete mode 100644 src/Discord.Net.Commands/CommandParameter.cs delete mode 100644 src/Discord.Net.Commands/CommandParser.cs delete mode 100644 src/Discord.Net.Commands/CommandService.cs delete mode 100644 src/Discord.Net.Commands/CommandServiceConfig.cs delete mode 100644 src/Discord.Net.Commands/Discord.Net.Commands.xproj delete mode 100644 src/Discord.Net.Commands/GenericPermissionChecker.cs delete mode 100644 src/Discord.Net.Commands/HelpMode.cs delete mode 100644 src/Discord.Net.Commands/IPermissionChecker.cs delete mode 100644 src/Discord.Net.Commands/project.json delete mode 100644 src/Discord.Net.Modules/Discord.Net.Modules.xproj delete mode 100644 src/Discord.Net.Modules/IModule.cs delete mode 100644 src/Discord.Net.Modules/ModuleChecker.cs delete mode 100644 src/Discord.Net.Modules/ModuleExtensions.cs delete mode 100644 src/Discord.Net.Modules/ModuleFilter.cs delete mode 100644 src/Discord.Net.Modules/ModuleManager.cs delete mode 100644 src/Discord.Net.Modules/ModuleService.cs delete mode 100644 src/Discord.Net.Modules/project.json delete mode 100644 src/Discord.Net.Shared/Discord.Net.Shared.projitems delete mode 100644 src/Discord.Net.Shared/Discord.Net.Shared.shproj delete mode 100644 src/Discord.Net.Shared/EpochTime.cs delete mode 100644 src/Discord.Net.Shared/TaskExtensions.cs delete mode 100644 src/Discord.Net.Shared/TaskHelper.cs delete mode 100644 src/Discord.Net/API/Client/Common/Channel.cs delete mode 100644 src/Discord.Net/API/Client/Common/ChannelReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/ExtendedMember.cs delete mode 100644 src/Discord.Net/API/Client/Common/Guild.cs delete mode 100644 src/Discord.Net/API/Client/Common/GuildReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/Invite.cs delete mode 100644 src/Discord.Net/API/Client/Common/InviteReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/Member.cs delete mode 100644 src/Discord.Net/API/Client/Common/MemberPresence.cs delete mode 100644 src/Discord.Net/API/Client/Common/MemberReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/Message.cs delete mode 100644 src/Discord.Net/API/Client/Common/MessageReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/RoleReference.cs delete mode 100644 src/Discord.Net/API/Client/Common/User.cs delete mode 100644 src/Discord.Net/API/Client/Common/UserReference.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs delete mode 100644 src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs delete mode 100644 src/Discord.Net/API/Client/ISerializable.cs delete mode 100644 src/Discord.Net/API/Client/Rest/AcceptInvite.cs delete mode 100644 src/Discord.Net/API/Client/Rest/AddGuildBan.cs delete mode 100644 src/Discord.Net/API/Client/Rest/DeleteRole.cs delete mode 100644 src/Discord.Net/API/Client/Rest/Gateway.cs delete mode 100644 src/Discord.Net/API/Client/Rest/GetBans.cs delete mode 100644 src/Discord.Net/API/Client/Rest/GetInvite.cs delete mode 100644 src/Discord.Net/API/Client/Rest/GetInvites.cs delete mode 100644 src/Discord.Net/API/Client/Rest/GetMessages.cs delete mode 100644 src/Discord.Net/API/Client/Rest/GetWidget.cs delete mode 100644 src/Discord.Net/API/Client/Rest/KickMember.cs delete mode 100644 src/Discord.Net/API/Client/Rest/Login.cs delete mode 100644 src/Discord.Net/API/Client/Rest/Logout.cs delete mode 100644 src/Discord.Net/API/Client/Rest/PruneMembers.cs delete mode 100644 src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs delete mode 100644 src/Discord.Net/API/Client/Rest/ReorderChannels.cs delete mode 100644 src/Discord.Net/API/Client/Rest/ReorderRoles.cs delete mode 100644 src/Discord.Net/API/Client/Rest/SendIsTyping.cs delete mode 100644 src/Discord.Net/API/Client/Rest/UpdateGuild.cs delete mode 100644 src/Discord.Net/API/Client/Rest/UpdateRole.cs delete mode 100644 src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs delete mode 100644 src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs delete mode 100644 src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs create mode 100644 src/Discord.Net/API/Common/Attachment.cs create mode 100644 src/Discord.Net/API/Common/Channel.cs create mode 100644 src/Discord.Net/API/Common/Embed.cs create mode 100644 src/Discord.Net/API/Common/EmbedProvider.cs create mode 100644 src/Discord.Net/API/Common/EmbedThumbnail.cs create mode 100644 src/Discord.Net/API/Common/Emoji.cs create mode 100644 src/Discord.Net/API/Common/Guild.cs create mode 100644 src/Discord.Net/API/Common/GuildEmbed.cs create mode 100644 src/Discord.Net/API/Common/GuildMember.cs create mode 100644 src/Discord.Net/API/Common/Integration.cs create mode 100644 src/Discord.Net/API/Common/IntegrationAccount.cs create mode 100644 src/Discord.Net/API/Common/Invite.cs create mode 100644 src/Discord.Net/API/Common/InviteChannel.cs create mode 100644 src/Discord.Net/API/Common/InviteGuild.cs create mode 100644 src/Discord.Net/API/Common/InviteMetadata.cs create mode 100644 src/Discord.Net/API/Common/Message.cs create mode 100644 src/Discord.Net/API/Common/ReadState.cs create mode 100644 src/Discord.Net/API/Common/Unconfirmed/Connection.cs rename src/Discord.Net/API/{Client/Common => Common/Unconfirmed}/ExtendedGuild.cs (95%) create mode 100644 src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs create mode 100644 src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs create mode 100644 src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs create mode 100644 src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs rename src/Discord.Net/API/{Client/Common => Common/Unconfirmed}/MemberVoiceState.cs (55%) create mode 100644 src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs create mode 100644 src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs rename src/Discord.Net/API/{Client/Common => Common/Unconfirmed}/Role.cs (77%) create mode 100644 src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs create mode 100644 src/Discord.Net/API/Common/User.cs create mode 100644 src/Discord.Net/API/Common/UserGuild.cs delete mode 100644 src/Discord.Net/API/Converters.cs delete mode 100644 src/Discord.Net/API/Extensions.cs rename src/Discord.Net/API/{Client/GatewaySocket/OpCodes.cs => GatewaySocket/OpCode.cs} (67%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Commands/Identify.cs (73%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Commands/RequestMembers.cs (53%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Commands/Resume.cs (69%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Commands/UpdateStatus.cs (74%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Commands/UpdateVoice.cs (51%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/ChannelCreate.cs (52%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/ChannelDelete.cs (54%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/ChannelUpdate.cs (54%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildBanAdd.cs (56%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildBanRemove.cs (57%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildCreate.cs (55%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildDelete.cs (55%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildMemberAdd.cs (57%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildMembersChunk.cs (51%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildRoleDelete.cs (57%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/GuildUpdate.cs (52%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/MessageAck.cs (56%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/MessageCreate.cs (54%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/MessageDelete.cs (57%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/MessageUpdate.cs (54%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/PresenceUpdate.cs (57%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/Ready.cs (96%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/Redirect.cs (77%) rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/Resumed.cs (79%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/UserUpdate.cs (51%) create mode 100644 src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs rename src/Discord.Net/API/{Client/GatewaySocket => GatewaySocket/Unconfirmed}/Events/VoiceStateUpdate.cs (58%) delete mode 100644 src/Discord.Net/API/IRestRequest.cs rename src/Discord.Net/API/{Client => }/IWebSocketMessage.cs (92%) create mode 100644 src/Discord.Net/API/Rest/AcceptInvite.cs rename src/Discord.Net/API/{Client => }/Rest/AckMessage.cs (59%) create mode 100644 src/Discord.Net/API/Rest/BeginGuildPrune.cs rename src/Discord.Net/API/{Client/Rest/CreateInvite.cs => Rest/CreateChannelInvite.cs} (62%) rename src/Discord.Net/API/{Client/Rest/CreatePrivateChannel.cs => Rest/CreateDMChannel.cs} (56%) rename src/Discord.Net/API/{Client => }/Rest/CreateGuild.cs (63%) create mode 100644 src/Discord.Net/API/Rest/CreateGuildBan.cs rename src/Discord.Net/API/{Client/Rest/CreateChannel.cs => Rest/CreateGuildChannel.cs} (73%) create mode 100644 src/Discord.Net/API/Rest/CreateGuildIntegration.cs rename src/Discord.Net/API/{Client/Rest/CreateRole.cs => Rest/CreateGuildRole.cs} (69%) rename src/Discord.Net/API/{Client/Rest/SendMessage.cs => Rest/CreateMessage.cs} (51%) rename src/Discord.Net/API/{Client => }/Rest/DeleteChannel.cs (69%) create mode 100644 src/Discord.Net/API/Rest/DeleteChannelPermission.cs rename src/Discord.Net/API/{Client => }/Rest/DeleteGuild.cs (69%) create mode 100644 src/Discord.Net/API/Rest/DeleteGuildIntegration.cs create mode 100644 src/Discord.Net/API/Rest/DeleteGuildRole.cs rename src/Discord.Net/API/{Client => }/Rest/DeleteInvite.cs (56%) rename src/Discord.Net/API/{Client => }/Rest/DeleteMessage.cs (67%) create mode 100644 src/Discord.Net/API/Rest/GetChannel.cs create mode 100644 src/Discord.Net/API/Rest/GetChannelInvites.cs create mode 100644 src/Discord.Net/API/Rest/GetChannelMessages.cs create mode 100644 src/Discord.Net/API/Rest/GetCurrentUser.cs create mode 100644 src/Discord.Net/API/Rest/GetCurrentUserConnections.cs create mode 100644 src/Discord.Net/API/Rest/GetCurrentUserDMs.cs create mode 100644 src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs create mode 100644 src/Discord.Net/API/Rest/GetGateway.cs create mode 100644 src/Discord.Net/API/Rest/GetGuild.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildBans.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildChannels.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildEmbed.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildIntegrations.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildInvites.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildMember.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildPruneCount.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildRoles.cs create mode 100644 src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs create mode 100644 src/Discord.Net/API/Rest/GetInvite.cs create mode 100644 src/Discord.Net/API/Rest/GetUser.cs rename src/Discord.Net/API/{Client => }/Rest/GetVoiceRegions.cs (65%) rename src/Discord.Net/API/{Client => }/Rest/LeaveGuild.cs (69%) create mode 100644 src/Discord.Net/API/Rest/ListGuildMembers.cs rename src/Discord.Net/API/{Client/Rest/AddChannelPermission.cs => Rest/ModifyChannelPermission.cs} (50%) rename src/Discord.Net/API/{Client/Rest/UpdateProfile.cs => Rest/ModifyCurrentUser.cs} (57%) create mode 100644 src/Discord.Net/API/Rest/ModifyGuild.cs rename src/Discord.Net/API/{Client/Rest/UpdateChannel.cs => Rest/ModifyGuildChannel.cs} (59%) create mode 100644 src/Discord.Net/API/Rest/ModifyGuildChannels.cs create mode 100644 src/Discord.Net/API/Rest/ModifyGuildEmbed.cs create mode 100644 src/Discord.Net/API/Rest/ModifyGuildIntegration.cs rename src/Discord.Net/API/{Client/Rest/UpdateMember.cs => Rest/ModifyGuildMember.cs} (50%) create mode 100644 src/Discord.Net/API/Rest/ModifyGuildRole.cs create mode 100644 src/Discord.Net/API/Rest/ModifyGuildRoles.cs rename src/Discord.Net/API/{Client/Rest/UpdateMessage.cs => Rest/ModifyMessage.cs} (77%) create mode 100644 src/Discord.Net/API/Rest/ModifyTextChannel.cs create mode 100644 src/Discord.Net/API/Rest/ModifyVoiceChannel.cs create mode 100644 src/Discord.Net/API/Rest/QueryUser.cs rename src/Discord.Net/API/{Client => }/Rest/RemoveGuildBan.cs (67%) create mode 100644 src/Discord.Net/API/Rest/RemoveGuildMember.cs create mode 100644 src/Discord.Net/API/Rest/SyncGuildIntegration.cs create mode 100644 src/Discord.Net/API/Rest/TriggerTypingIndicator.cs rename src/Discord.Net/API/{Client/Rest => Rest/Unconfirmed}/SendFile.cs (50%) delete mode 100644 src/Discord.Net/API/Status/Common/StatusResult.cs delete mode 100644 src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs delete mode 100644 src/Discord.Net/API/Status/Rest/AllIncidents.cs delete mode 100644 src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs delete mode 100644 src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs rename src/Discord.Net/API/{Client/VoiceSocket/OpCodes.cs => VoiceSocket/OpCode.cs} (81%) create mode 100644 src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs create mode 100644 src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs create mode 100644 src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs rename src/Discord.Net/API/{Client/VoiceSocket => VoiceSocket/Unconfirmed}/Commands/SetSpeaking.cs (66%) rename src/Discord.Net/API/{Client/VoiceSocket => VoiceSocket/Unconfirmed}/Events/Ready.cs (90%) rename src/Discord.Net/API/{Client/VoiceSocket => VoiceSocket/Unconfirmed}/Events/SessionDescription.cs (85%) rename src/Discord.Net/API/{Client/VoiceSocket => VoiceSocket/Unconfirmed}/Events/Speaking.cs (57%) create mode 100644 src/Discord.Net/CDN.cs create mode 100644 src/Discord.Net/Discord.Net.Net45.csproj create mode 100644 src/Discord.Net/Discord.Net.Net45.project.json create mode 100644 src/Discord.Net/Discord.Net.Net45.project.lock.json delete mode 100644 src/Discord.Net/DiscordClient.Events.cs create mode 100644 src/Discord.Net/DiscordSocketClient.cs delete mode 100644 src/Discord.Net/DynamicIL.cs delete mode 100644 src/Discord.Net/ETF/ETFReader.cs delete mode 100644 src/Discord.Net/ETF/ETFType.cs delete mode 100644 src/Discord.Net/ETF/ETFWriter.cs create mode 100644 src/Discord.Net/Entities/Attachment.cs delete mode 100644 src/Discord.Net/Entities/Channel.cs create mode 100644 src/Discord.Net/Entities/Channels/DMChannel.cs create mode 100644 src/Discord.Net/Entities/Channels/GuildChannel.cs rename {ref => src/Discord.Net}/Entities/Channels/IChannel.cs (80%) rename ref/Entities/Channels/ITextChannel.cs => src/Discord.Net/Entities/Channels/IMessageChannel.cs (95%) create mode 100644 src/Discord.Net/Entities/Channels/TextChannel.cs create mode 100644 src/Discord.Net/Entities/Channels/VoiceChannel.cs create mode 100644 src/Discord.Net/Entities/Embed.cs create mode 100644 src/Discord.Net/Entities/EmbedProvider.cs create mode 100644 src/Discord.Net/Entities/EmbedThumbnail.cs create mode 100644 src/Discord.Net/Entities/Emoji.cs create mode 100644 src/Discord.Net/Entities/Guild.cs create mode 100644 src/Discord.Net/Entities/Helpers/InviteManager.cs create mode 100644 src/Discord.Net/Entities/Helpers/MentionHelper.cs create mode 100644 src/Discord.Net/Entities/Helpers/MessageManager.cs create mode 100644 src/Discord.Net/Entities/Helpers/PermissionManager.cs create mode 100644 src/Discord.Net/Entities/Helpers/PermissionsHelper.cs delete mode 100644 src/Discord.Net/Entities/IChannel.cs rename {ref => src/Discord.Net}/Entities/IEntity.cs (81%) rename {ref => src/Discord.Net}/Entities/IMentionable.cs (100%) delete mode 100644 src/Discord.Net/Entities/IPrivateChannel.cs delete mode 100644 src/Discord.Net/Entities/IPublicChannel.cs delete mode 100644 src/Discord.Net/Entities/ITextChannel.cs delete mode 100644 src/Discord.Net/Entities/IVoiceChannel.cs delete mode 100644 src/Discord.Net/Entities/Invite.cs create mode 100644 src/Discord.Net/Entities/Invites/GuildInvite.cs create mode 100644 src/Discord.Net/Entities/Invites/IInvite.cs create mode 100644 src/Discord.Net/Entities/Invites/Invite.cs create mode 100644 src/Discord.Net/Entities/Invites/InviteChannel.cs create mode 100644 src/Discord.Net/Entities/Invites/InviteGuild.cs delete mode 100644 src/Discord.Net/Entities/Managers/MessageManager.cs delete mode 100644 src/Discord.Net/Entities/Managers/PermissionManager.cs delete mode 100644 src/Discord.Net/Entities/Permissions.cs create mode 100644 src/Discord.Net/Entities/Permissions/ChannelPermissions.cs create mode 100644 src/Discord.Net/Entities/Permissions/GuildPermissions.cs create mode 100644 src/Discord.Net/Entities/Permissions/Overwrite.cs create mode 100644 src/Discord.Net/Entities/Permissions/OverwritePermissions.cs create mode 100644 src/Discord.Net/Entities/Presences/GuildPresence.cs create mode 100644 src/Discord.Net/Entities/Presences/Presence.cs create mode 100644 src/Discord.Net/Entities/Presences/VoiceState.cs delete mode 100644 src/Discord.Net/Entities/PrivateChannel.cs delete mode 100644 src/Discord.Net/Entities/Profile.cs delete mode 100644 src/Discord.Net/Entities/PublicChannel.cs delete mode 100644 src/Discord.Net/Entities/Region.cs delete mode 100644 src/Discord.Net/Entities/Server.cs delete mode 100644 src/Discord.Net/Entities/TextChannel.cs delete mode 100644 src/Discord.Net/Entities/User.cs create mode 100644 src/Discord.Net/Entities/Users/DMUser.cs create mode 100644 src/Discord.Net/Entities/Users/GuildUser.cs create mode 100644 src/Discord.Net/Entities/Users/PublicUser.cs create mode 100644 src/Discord.Net/Entities/Users/SelfUser.cs create mode 100644 src/Discord.Net/Entities/Users/User.cs delete mode 100644 src/Discord.Net/Entities/VoiceChannel.cs create mode 100644 src/Discord.Net/Entities/VoiceRegion.cs delete mode 100644 src/Discord.Net/EnumConverters.cs delete mode 100644 src/Discord.Net/Enums/ImageType.cs delete mode 100644 src/Discord.Net/Enums/MessageState.cs delete mode 100644 src/Discord.Net/Enums/StringEnum.cs create mode 100644 src/Discord.Net/Events/CurrentUserEventArgs.cs create mode 100644 src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs create mode 100644 src/Discord.Net/Events/GuildEventArgs.cs create mode 100644 src/Discord.Net/Events/GuildUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/Events/ProfileUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/Events/ServerEventArgs.cs delete mode 100644 src/Discord.Net/Events/ServerUpdatedEventArgs.cs create mode 100644 src/Discord.Net/Events/VoiceChannelEventArgs.cs delete mode 100644 src/Discord.Net/IMentionable.cs delete mode 100644 src/Discord.Net/IModel.cs delete mode 100644 src/Discord.Net/IService.cs delete mode 100644 src/Discord.Net/Logging/ILogger.cs create mode 100644 src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/ImageConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/UInt64Converter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs create mode 100644 src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs delete mode 100644 src/Discord.Net/Net/Rest/BuiltInEngine.cs delete mode 100644 src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs create mode 100644 src/Discord.Net/Net/Rest/DefaultRestEngine.cs delete mode 100644 src/Discord.Net/Net/Rest/ETFRestClient.cs rename {ref => src/Discord.Net}/Net/Rest/IRestRequest.cs (53%) delete mode 100644 src/Discord.Net/Net/Rest/JsonRestClient.cs delete mode 100644 src/Discord.Net/Net/Rest/RequestEventArgs.cs create mode 100644 src/Discord.Net/Net/Rest/RestClientProvider.cs create mode 100644 src/Discord.Net/Net/Rest/RestParameter.cs create mode 100644 src/Discord.Net/Net/Rest/SentRequestEventArgs.cs delete mode 100644 src/Discord.Net/Net/Rest/SharpRestEngine.cs delete mode 100644 src/Discord.Net/Net/TimeoutException.cs delete mode 100644 src/Discord.Net/Net/WebSocketException.cs rename src/Discord.Net/Net/WebSockets/{BuiltInEngine.cs => DefaultWebSocketEngine.cs} (59%) delete mode 100644 src/Discord.Net/Net/WebSockets/WS4NetEngine.cs create mode 100644 src/Discord.Net/Net/WebSockets/WebSocketProvider.cs create mode 100644 src/Discord.Net/Properties/AssemblyInfo.cs delete mode 100644 src/Discord.Net/ServiceCollection.cs create mode 100644 src/Discord.Net/TaskHelper.cs delete mode 100644 src/Discord.Net/TaskManager.cs delete mode 100644 test/Discord.Net.Tests/Settings.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index db11e8b30..bfcda2fab 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,17 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8D7989F0-66CE-4DBB-8230-D8C811E9B1D7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "netplatform", "netplatform", "{EA68EBE2-51C8-4440-9EF7-D633C90A5D35}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{ACFB060B-EC8A-4926-B293-04C01E17EE23}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{19793545-EF89-48F4-8100-3EBAAD0A9141}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{2C91BDD7-621D-460F-B768-EAD106D9BA62}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6317A2E6-8E36-4C3E-949B-3F10EC888AB9}" EndProject @@ -22,104 +18,44 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net.Net45\Discord.Net.csproj", "{8D71A857-879A-4A10-859E-5FF824ED6688}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Commands", "src\Discord.Net.Commands.Net45\Discord.Net.Commands.csproj", "{1B5603B4-6F8F-4289-B945-7BAAE523D740}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\Discord.Net.Modules\Discord.Net.Modules.xproj", "{01584E8A-78DA-486F-9EF9-A894E435841B}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Audio", "src\Discord.Net.Audio\Discord.Net.Audio.xproj", "{DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}" -EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Discord.Net.Shared", "src\Discord.Net.Shared\Discord.Net.Shared.shproj", "{2875DEB5-F248-4105-8EA2-5141E3DE8025}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Audio", "src\Discord.Net.Audio.Net45\Discord.Net.Audio.csproj", "{7BFEF748-B934-4621-9B11-6302E3A9F6B3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Net45", "src\Discord.Net\Discord.Net.Net45.csproj", "{C6A50D24-CBD3-4E76-852C-4DCA60BBD608}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{2875deb5-f248-4105-8ea2-5141e3de8025}*SharedItemsImports = 13 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{7bfef748-b934-4621-9b11-6302e3a9f6b3}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{1b5603b4-6f8f-4289-b945-7baae523d740}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{3091164f-66ae-4543-a63d-167c1116241d}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{8d71a857-879a-4a10-859e-5ff824ed6688}*SharedItemsImports = 4 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU FullDebug|Any CPU = FullDebug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.Build.0 = Release|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.Build.0 = Release|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Release|Any CPU.Build.0 = Release|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.Build.0 = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.ActiveCfg = Release|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.Build.0 = Release|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.Build.0 = Release|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.Build.0 = Release|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Release|Any CPU.Build.0 = Release|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Release|Any CPU.Build.0 = Release|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.Build.0 = Release|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} - {ACFB060B-EC8A-4926-B293-04C01E17EE23} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {19793545-EF89-48F4-8100-3EBAAD0A9141} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} + {2C91BDD7-621D-460F-B768-EAD106D9BA62} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {855D6B1D-847B-42DA-BE6A-23683EA89511} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} - {8D71A857-879A-4A10-859E-5FF824ED6688} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {2875DEB5-F248-4105-8EA2-5141E3DE8025} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {7BFEF748-B934-4621-9B11-6302E3A9F6B3} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} + {628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608} = {628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA} EndGlobalSection EndGlobal diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000..91bc2845a --- /dev/null +++ b/NuGet.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/features/commands.rst b/docs/features/commands.rst index e6bd6bf82..8abfb18a9 100644 --- a/docs/features/commands.rst +++ b/docs/features/commands.rst @@ -1,7 +1,7 @@ |stub| Commands =============== -The `Discord.Net.Commands`_ package extends DiscordClient with a built-in Commands Handler. +The `Discord.Net.Commands`_ package DiscordBotClient extends DiscordClient with support for commands. .. _Discord.Net.Commands: https://www.nuget.org/packages/Discord.Net.Commands diff --git a/docs/features/events.rst b/docs/features/events.rst index d782c438e..2cfe27f54 100644 --- a/docs/features/events.rst +++ b/docs/features/events.rst @@ -3,19 +3,21 @@ Events Usage ----- -Messages from the Discord server are exposed via events on the DiscordClient class and follow the standard EventHandler C# pattern. +Messages from the Discord server are exposed via events on the DiscordClient class and follow the standard EventHandler C# pattern. .. warning:: - Note that all synchronous code in an event handler will run on the gateway socket's thread and should be handled as quickly as possible. + Note that all synchronous code in an event handler will run on the gateway socket's thread and should be handled as quickly as possible. Using the async-await pattern to let the thread continue immediately is recommended and is demonstrated in the examples below. -Ready ------ - -The Ready Event is raised only once, when your client finishes processing the READY packet from Discord. +Connection State +---------------- -This has replaced the previous "Connected" event, and indicates that it is safe to begin retrieving users, channels, or servers from the cache. +Connection Events will be raised when the Connection State of your client changes. +.. warning:: + You should not use DiscordClient.Connected to run code when your client first connects to Discord. + If you lose connection and automatically reconnect, this code will be ran again, which may lead to unexpected behavior. + Messages -------- @@ -24,7 +26,7 @@ Messages Example of MessageReceived: -.. code-block:: csharp6 +.. code-block:: c# // (Preface: Echo Bots are discouraged, make sure your bot is not running in a public server if you use them) @@ -54,7 +56,7 @@ There are several user events: Examples: -.. code-block:: csharp6 +.. code-block:: c# // Register a Hook into the UserBanned event using a Lambda _client.UserBanned += async (s, e) => { diff --git a/docs/features/logging.rst b/docs/features/logging.rst index c2f9d6ce3..4b9f254a5 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -2,12 +2,12 @@ Logging ======= Discord.Net will log all of its events/exceptions using a built-in LogManager. -This LogManager can be accessed through ``DiscordClient.Log`` +This LogManager can be accessed through DiscordClient.Log Usage ----- -To handle Log Messages through Discord.Net's Logger, you must hook into the ``Log.Message`` Event. +To handle Log Messages through Discord.Net's Logger, you must hook into the Log.Message Event. The LogManager does not provide a string-based result for the message, you must put your own message format together using the data provided through LogMessageEventArgs See the Example for a snippet of logging. @@ -17,25 +17,19 @@ Logging Your Own Data The LogManager included in Discord.Net can also be used to log your own messages. -You can use ``DiscordClient.Log.Log(LogSeverity, Source, Message, [Exception])``, or one of the shortcut helpers, to log data. +You can use DiscordClient.Log.Log(LogSeverity, Source, Message, Exception), or one of the shortcut helpers, to log data. Example: - -.. code-block:: csharp6 +.. code-block:: c# _client.MessageReceived += async (s, e) { // Log a new Message with Severity Info, Sourced from 'MessageReceived', with the Message Contents. _client.Log.Info("MessageReceived", e.Message.Text, null); }; - -.. warning:: - - Starting in Discord.Net 1.0, you will not be able to log your own messages. You will need to create your own Logging manager, or use a pre-existing one. - Example ------- .. literalinclude:: /samples/logging.cs - :language: csharp6 + :language: c# :tab-width: 2 diff --git a/docs/features/permissions.rst b/docs/features/permissions.rst index aa3856807..058fe07cf 100644 --- a/docs/features/permissions.rst +++ b/docs/features/permissions.rst @@ -1,21 +1,8 @@ Permissions =========== -|outdated| - There are two types of permissions: *Channel Permissions* and *Server Permissions*. -Permission Overrides --------------------- - -Channel Permissions are expressed using an enum, ``PermValue``. - -The three states are fairly straightforward - - -``PermValue.Allow``: Allow the user to perform a permission. -``PermValue.Deny``: Deny the user to perform a permission. -``PermValue.Inherit``: The user will inherit the permission from its role. - Channel Permissions ------------------- Channel Permissions are controlled using a set of flags: @@ -42,24 +29,27 @@ Speak Voice Speak in a voice channel. UseVoiceActivation Voice Use Voice Activation in a text channel (for large channels where PTT is preferred) ======================= ======= ============== -Each flag is a PermValue; see the section above. +If a user has a permission, the value is true. Otherwise, it must be null. -Setting Channel Permissions ---------------------------- +Dual Channel Permissions +------------------------ +You may also access a user's permissions in a channel with the DualChannelPermissions class. +Unlike normal ChannelPermissions, DualChannelPermissions hold three values: -To set channel permissions, create a new ``ChannelPermissionOverrides``, and specify the flags/values that you want to override. +If a user has a permission, the value is true. If a user is denied a permission, it will be false. If the permission is not set, the value will return null. -Then, update the user, by doing ``Channel.AddPermissionsRule(_user, _overwrites);`` +Setting Channel Permissions +--------------------------- -Roles ------ +To set channel permissions, you may use either two ChannelPermissions, or one DualChannelPermissions. -Accessing/modifying permissions for roles is done the same way as user permissions, just using the overload for a Role. See above sections. +In the case of using two Channel Permissions, you must create one list of allowed permissions, and one list of denied permissions. +Otherwise, you can use a single DualChannelPermissions. Server Permissions ------------------ -Server Permissions can be viewed with ``User.ServerPermissions``, but **at the time of this writing** cannot be set. +Server Permissions can be accessed by ``Server.GetPermissions(User)``, and updated with ``Server.UpdatePermissions(User, ServerPermissions)`` A user's server permissions also contain the default values for it's channel permissions, so the channel permissions listed above are also valid flags for Server Permissions. There are also a few extra Server Permissions: @@ -71,7 +61,11 @@ KickMembers Server Kick users from the server. They can still rejoi ManageRoles Server Manage roles on the server, and their permissions. ManageChannels Server Manage channels that exist on the server (add, remove them) ManageServer Server Manage the server settings. -======================= ======= ============== + +Roles +----- + +Managing permissions for roles is much easier than for users in channels. For roles, just access the flag under `Role.Permissions`. Example ------- diff --git a/docs/features/server-management.rst b/docs/features/server-management.rst index 765fd4e0f..d555875a8 100644 --- a/docs/features/server-management.rst +++ b/docs/features/server-management.rst @@ -10,7 +10,7 @@ You can create Channels, Invites, and Roles on a server using the CreateChannel, You may also edit a server's name, icon, and region. -.. code-block:: csharp6 +.. code-block:: c# // Create a Channel and retrieve the Channel object var _channel = await _server.CreateChannel("announcements", ChannelType.Text); diff --git a/docs/features/user-management.rst b/docs/features/user-management.rst index cf3305312..972b3ab4b 100644 --- a/docs/features/user-management.rst +++ b/docs/features/user-management.rst @@ -6,7 +6,7 @@ Banning To ban a user, invoke the Ban function on a Server object. -.. code-block:: csharp6 +.. code-block:: c# _server.Ban(_user, 30); @@ -17,6 +17,6 @@ Kicking To kick a user, invoke the Kick function on the User. -.. code-block:: csharp6 +.. code-block:: c# _user.Kick(); diff --git a/docs/features/voice.rst b/docs/features/voice.rst index a4dddeff5..fc6867b58 100644 --- a/docs/features/voice.rst +++ b/docs/features/voice.rst @@ -1,180 +1,13 @@ -Voice -===== +|stub| Voice +================= -Installation ------------- - -Before setting up the AudioService, you must first install the package `from NuGet`_ or `GitHub`_. - -Add the package to your solution, and then import the namespace ``Discord.Audio``. - -.. _from NuGet: https://www.nuget.org/packages/Discord.Net.Audio/0.9.0-rc3 -.. _GitHub: https://github.com/RogueException/Discord.Net/tree/master/src/Discord.Net.Audio - -Setup ------ - -To use audio, you must install the AudioService to your DiscordClient. - -.. code-block:: csharp6 - - var _client = new DiscordClient(); - - _client.UsingAudio(x => // Opens an AudioConfigBuilder so we can configure our AudioService - { - x.Mode = AudioMode.Outgoing; // Tells the AudioService that we will only be sending audio - }); - -Joining a Channel ------------------ - -Joining Voice Channels is pretty straight-forward, and is required to send Audio. This will also allow us to get an IAudioClient, which we will later use to send Audio. - -.. code-block:: csharp6 - - var voiceChannel = _client.FindServers("Music Bot Server").FirstOrDefault().VoiceChannels.FirstOrDefault(); // Finds the first VoiceChannel on the server 'Music Bot Server' - - var _vClient = await _client.GetService() // We use GetService to find the AudioService that we installed earlier. In previous versions, this was equivelent to _client.Audio() - .Join(VoiceChannel); // Join the Voice Channel, and return the IAudioClient. - -The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to Disconnect. - -The IAudioClient ----------------- - -The IAudioClient is used to connect/disconnect to/from a Voice Channel, and to send audio to that Voice Channel. - -.. function:: IAudioClient.Disconnect(); - - Disconnects the IAudioClient from the Voice Server. - - -.. function:: IAudioClient.Join(Channel); - - Moves the IAudioClient to another channel on the Voice Server, or starts a connection if one has already been terminated. - -.. note:: - - Because versions previous to 0.9 do not discretely differentiate between Text and Voice Channels, you may want to ensure that users cannot request the audio client to join a text channel, as this will throw an exception, leading to potentially unexpected behavior - -.. function:: IAudioClient.Wait(); - - Blocks the current thread until the sending audio buffer has cleared out. - -.. function:: IAudioClient.Clear(); - - Clears the sending audio buffer. - -.. function:: IAudioClient.Send(byte[] data, int offset, int count); - - Adds a stream of data to the Audio Client's internal buffer, to be sent to Discord. Follows the standard c# Stream.Send() format. +|stub-desc| Broadcasting ------------ -There are multiple approaches to broadcasting audio. Discord.Net will convert your audio packets into Opus format, so the only work you need to do is converting your audio into a format that Discord will accept. The format Discord takes is 16-bit 48000Hz PCM. - -Broadcasting with NAudio ------------------------- - -`NAudio`_ is one of the easiest approaches to sending audio, although it is not multi-platform compatible. The following example will show you how to read an mp3 file, and send it to Discord. -You can `download NAudio from NuGet`_. - -.. code-block:: csharp6 - - using NAudio; - using NAudio.Wave; - using NAudio.CoreAudioApi; - - public void SendAudio(string filePath) - { - var channelCount = _client.GetService().Config.Channels; // Get the number of AudioChannels our AudioService has been configured to use. - var OutFormat = new WaveFormat(48000, 16, channelCount); // Create a new Output Format, using the spec that Discord will accept, and with the number of channels that our client supports. - using (var MP3Reader = new Mp3FileReader(filePath)) // Create a new Disposable MP3FileReader, to read audio from the filePath parameter - using (var resampler = new MediaFoundationResampler(MP3Reader, OutFormat)) // Create a Disposable Resampler, which will convert the read MP3 data to PCM, using our Output Format - { - resampler.ResamplerQuality = 60; // Set the quality of the resampler to 60, the highest quality - int blockSize = outFormat.AverageBytesPerSecond / 50; // Establish the size of our AudioBuffer - byte[] buffer = new byte[blockSize]; - int byteCount; - - while((byteCount = resampler.Read(buffer, 0, blockSize)) > 0) // Read audio into our buffer, and keep a loop open while data is present - { - if (byteCount < blockSize) - { - // Incomplete Frame - for (int i = byteCount; i < blockSize; i++) - buffer[i] = 0; - } - _vClient.Send(buffer, 0, blockSize); // Send the buffer to Discord - } - } - - } - -.. _NAudio: https://naudio.codeplex.com/ -.. _download NAudio from NuGet: https://www.nuget.org/packages/NAudio/ - -Broadcasting with FFmpeg ------------------------- - -`FFmpeg`_ allows for a more advanced approach to sending audio, although it is multiplatform safe. The following example will show you how to stream a file to Discord. - -.. code-block:: csharp6 - - public void SendAudio(string pathOrUrl) - { - var process = Process.Start(new ProcessStartInfo { // FFmpeg requires us to spawn a process and hook into its stdout, so we will create a Process - FileName = "ffmpeg", - Arguments = $"-i {pathOrUrl}" + // Here we provide a list of arguments to feed into FFmpeg. -i means the location of the file/URL it will read from - "-f s16le -ar 48000 -ac 2 pipe:1", // Next, we tell it to output 16-bit 48000Hz PCM, over 2 channels, to stdout. - UseShellExecute = false, - RedirectStandardOutput = true // Capture the stdout of the process - }); - Thread.Sleep(2000); // Sleep for a few seconds to FFmpeg can prebuffer. - - int blockSize = 3840; // The size of bytes to read per frame; 1920 for mono - byte[] buffer = new byte[blockSize]; - int byteCount; - - while (true) // Loop forever, so data will always be read - { - byteCount = process.StandardOutput.BaseStream // Access the underlying MemoryStream from the stdout of FFmpeg - .Read(buffer, 0, blockSize); // Read stdout into the buffer - - if (byteCount == 0) // FFmpeg did not output anything - break; // Break out of the while(true) loop, since there was nothing to read. - - _vClient.Send(buffer, 0, byteCount); // Send our data to Discord - } - _vClient.Wait(); // Wait for the Voice Client to finish sending data, as ffMPEG may have already finished buffering out a song, and it is unsafe to return now. - } - -.. _FFmpeg: https://ffmpeg.org/ - -.. note:: - - The code-block above assumes that your client is configured to stream 2-channel audio. It also may prematurely end a song. FFmpeg can — especially when streaming from a URL — stop to buffer data from a source, and cause your output stream to read empty data. Because the snippet above does not safely track for failed attempts, or buffers, an empty buffer will cause playback to stop. This is also not 'memory-friendly'. - Multi-Server Broadcasting ------------------------- -.. warning:: Multi-Server broadcasting is not supported by Discord, will cause performance issues for you, and is not encouraged. Proceed with caution. - -To prepare for Multi-Server Broadcasting, you must first enable it in your config. - -.. code-block::csharp6 - - _client.UsingAudio(x => - { - x.Mode = AudioMode.Outgoing; - x.EnableMultiserver = true; // Enable Multiserver - }); - -From here on, it is as easy as creating an IAudioClient for each server you want to join. See the sections on broadcasting to proceed. - - Receiving ---------- - -**Receiving is not implemented in the latest version of Discord.Net** \ No newline at end of file +--------- \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f86b36231..f9dfd857d 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -22,12 +22,12 @@ You can get Discord.Net from NuGet: If you have trouble installing from NuGet, try installing dependencies manually. -You can also pull the latest source from `GitHub`_ +You can also pull the latest source from `GitHub`_ .. _Discord.Net: https://www.nuget.org/packages/Discord.Net .. _Discord.Net.Commands: https://www.nuget.org/packages/Discord.Net.Commands .. _Discord.Net.Modules: https://www.nuget.org/packages/Discord.Net.Modules -.. _Discord.Net.Audio: https://www.nuget.org/packages/Discord.Net.Audio +.. _Discord.Net.Modules: https://www.nuget.org/packages/Discord.Net.Audio .. _GitHub: https://github.com/RogueException/Discord.Net/ Async @@ -42,7 +42,7 @@ For more information, go to `MSDN's Await-Async section`_. Example ------- - + .. literalinclude:: samples/getting_started.cs :language: csharp6 :tab-width: 2 diff --git a/docs/global.txt b/docs/global.txt index 25b33510e..e5b572c93 100644 --- a/docs/global.txt +++ b/docs/global.txt @@ -1,4 +1,2 @@ .. |stub| unicode:: U+1F527 -.. |stub-desc| replace:: This page is a placeholder and has not been written yet. It should be coming soon! -.. |outdated| replace:: **This page is currently out-of-date. The information below may be inaccurate.** -.. |incomplete| replace:: **This page is incomplete. While the information below is accurate, it should be noted that it is not thorough.** \ No newline at end of file +.. |stub-desc| replace:: This page is a placeholder and has not been written yet. It should be coming soon! \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index bf5676406..d2ff662af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,13 +9,13 @@ Feel free to join us in the `Discord API chat`_. .. _Discord chat service: https://discordapp.com .. _Discord API chat: https://discord.gg/0SBTUU1wZTVjAMPx -.. warning:: +.. warn:: - This is a beta! +This is a beta! - This library has been built thanks to a community effort reverse engineering the Discord client. - As the API is still unofficial, it may change at any time without notice, breaking this library as well. - Discord.Net itself is still in development (and is currently undergoing a rewrite) and you may encounter breaking changes throughout development until the official Discord API is released. +This library has been built thanks to a community effort reverse engineering the Discord client. +As the API is still unofficial, it may change at any time without notice, breaking this library as well. +Discord.Net itself is still in development (and is currently undergoing a rewrite) and you may encounter breaking changes throughout development until the official Discord API is released. It is highly recommended that you always use the latest version and please report any bugs you find to our `Discord chat`_. @@ -23,8 +23,6 @@ It is highly recommended that you always use the latest version and please repor This Documentation is **currently undergoing a rewrite**. Some pages (marked with a wrench) are not updated, or are not completed yet. -**The documentation is currently being written to reflect ``0.9-rc4``, which can be accessed via the latest git-master.** - .. toctree:: :caption: Documentation :maxdepth: 2 diff --git a/docs/samples/events.cs b/docs/samples/events.cs index 8a53c0bbc..7f68bf6cb 100644 --- a/docs/samples/events.cs +++ b/docs/samples/events.cs @@ -1,20 +1,20 @@ class Program { - private static DiscordClient _client; + private static DiscordBotClient _client; static void Main(string[] args) { - _client = new DiscordClient(); + var client = new DiscordClient(); // Handle Events using Lambdas - _client.MessageReceived += (s, e) => + client.MessageCreated += (s, e) => { if (!e.Message.IsAuthor) - await e.Channel.SendMessage("foo"); + await client.SendMessage(e.Message.ChannelId, "foo"); } // Handle Events using Event Handlers EventHandler handler = new EventHandler(HandleMessageCreated); - client.MessageReceived += handler; + client.MessageCreated += handler; } @@ -22,6 +22,6 @@ class Program static void HandleMessageCreated(object sender, EventArgs e) { if (!e.Message.IsAuthor) - await e.Channel.SendMessage("bar"); + await client.SendMessage(e.Message.ChannelId, "foo"); } -} +} \ No newline at end of file diff --git a/docs/samples/getting_started.cs b/docs/samples/getting_started.cs index d471fbc65..55f7923a4 100644 --- a/docs/samples/getting_started.cs +++ b/docs/samples/getting_started.cs @@ -2,13 +2,10 @@ class Program { static void Main(string[] args) { - var client = new DiscordClient(x => - { - LogLevel = LogSeverity.Info - }); + var client = new DiscordClient(); //Display all log messages in the console - client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); + client.LogMessage += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); //Echo back any message received, provided it didn't come from the bot itself client.MessageReceived += async (s, e) => @@ -25,7 +22,7 @@ class Program //If we are not a member of any server, use our invite code (made beforehand in the official Discord Client) if (!client.Servers.Any()) - await (client.GetInvite("aaabbbcccdddeee")).Accept(); + await client.AcceptInvite(client.GetInvite("aaabbbcccdddeee")); }); } } diff --git a/docs/samples/logging.cs b/docs/samples/logging.cs index 4fd3e4959..c68b8aded 100644 --- a/docs/samples/logging.cs +++ b/docs/samples/logging.cs @@ -1,5 +1,6 @@ class Program { + private static DiscordBotClient _client; static void Main(string[] args) { var client = new DiscordClient(x => @@ -7,13 +8,13 @@ class Program LogLevel = LogSeverity.Info }); - client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); + _client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); client.ExecuteAndWait(async () => { await client.Connect("discordtest@email.com", "Password123"); if (!client.Servers.Any()) - await (client.GetInvite("aaabbbcccdddeee")).Accept(); + await client.AcceptInvite("aaabbbcccdddeee"); }); } } diff --git a/docs/samples/permissions.cs b/docs/samples/permissions.cs index 65681d0f9..419026714 100644 --- a/docs/samples/permissions.cs +++ b/docs/samples/permissions.cs @@ -1,8 +1,14 @@ - // Find a User's Channel Permissions -var UserPerms = _channel.GetPermissionsRule(_user); +var userChannelPermissions = user.GetPermissions(channel); + +// Find a User's Server Permissions +var userServerPermissions = user.ServerPermissions(); +var userServerPermissions = server.GetPermissions(user); -// Set a User's Channel Permissions +// Set a User's Channel Permissions (using DualChannelPermissions) -var NewOverwrites = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); -await channel.AddPermissionsRule(_user, NewOverwrites); +var userPerms = user.GetPermissions(channel); +userPerms.ReadMessageHistory = false; +userPerms.AttachFiles = null; +channel.AddPermissionsRule(user, userPerms); +} diff --git a/global.json b/global.json index 4357be0d5..7f3ac9f7e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "projects": [ "src" ], - "sdk": { - "version": "1.0.0-rc1-update1" - } + "projects": [ "src" ], + "sdk": { + "version": "1.0.0-rc2-20221" + } } \ No newline at end of file diff --git a/ref/Discord.Net.xproj b/ref/Discord.Net.xproj deleted file mode 100644 index d3559797d..000000000 --- a/ref/Discord.Net.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 5b2afee6-fff6-4ba2-be12-61b283b72ac0 - Discord - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/ref/DiscordClient.cs b/ref/DiscordClient.cs deleted file mode 100644 index aa777e04f..000000000 --- a/ref/DiscordClient.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - /// Provides a connection to the DiscordApp service. - public class DiscordClient : IDisposable - { - public event EventHandler Log = delegate { }; - - public event EventHandler LoggedIn = delegate { }; - public event EventHandler LoggedOut = delegate { }; - public event EventHandler Connected = delegate { }; - public event EventHandler Disconnected = delegate { }; - public event EventHandler VoiceConnected = delegate { }; - public event EventHandler VoiceDisconnected = delegate { }; - - public event EventHandler ChannelCreated = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; - public event EventHandler ChannelDestroyed = delegate { }; - public event EventHandler MessageAcknowledged = delegate { }; - public event EventHandler MessageDeleted = delegate { }; - public event EventHandler MessageReceived = delegate { }; - public event EventHandler MessageSent = delegate { }; - public event EventHandler MessageUpdated = delegate { }; - public event EventHandler ProfileUpdated = delegate { }; - public event EventHandler RoleCreated = delegate { }; - public event EventHandler RoleUpdated = delegate { }; - public event EventHandler RoleDeleted = delegate { }; - public event EventHandler JoinedServer = delegate { }; - public event EventHandler LeftServer = delegate { }; - public event EventHandler ServerAvailable = delegate { }; - public event EventHandler ServerUpdated = delegate { }; - public event EventHandler ServerUnavailable = delegate { }; - public event EventHandler UserBanned = delegate { }; - public event EventHandler UserIsTyping = delegate { }; - public event EventHandler UserJoined = delegate { }; - public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserUnbanned = delegate { }; - - public MessageQueue MessageQueue { get; } - public IRestClient RestClient { get; } - public GatewaySocket GatewaySocket { get; } - public Profile CurrentUser { get; } - - public DiscordClient() { } - public DiscordClient(DiscordConfig config) { } - - public Task Login(string token) => null; - public Task Logout() => null; - - public Task Connect() => null; - public Task Connect(int connectionId, int totalConnections) => null; - public Task Disconnect() => null; - - public Task> GetPrivateChannels() => null; - public Task GetPrivateChannel(ulong userId) => null; - public Task GetInvite(string inviteIdOrXkcd) => null; - public Task> GetRegions() => null; - public Task GetRegion(string id) => null; - public Task> GetServers() => null; - public Task GetServer(ulong id) => null; - - public Task CreatePrivateChannel(ulong userId) => null; - public Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) => null; - - public void Dispose() { } - } -} \ No newline at end of file diff --git a/ref/DiscordConfig.cs b/ref/DiscordConfig.cs deleted file mode 100644 index e6b1a5568..000000000 --- a/ref/DiscordConfig.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System.Reflection; - -namespace Discord -{ - public class DiscordConfig - { - public const int MaxMessageSize = 2000; - public const int MaxMessagesPerBatch = 100; - - public const string LibName = "Discord.Net"; - public static string LibVersion => typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; - public const string LibUrl = "https://github.com/RogueException/Discord.Net"; - - public const string ClientAPIUrl = "https://discordapp.com/api/"; - public const string CDNUrl = "https://cdn.discordapp.com/"; - public const string InviteUrl = "https://discord.gg/"; - - /// Gets or sets name of your application, used in the user agent. - public string AppName { get; set; } = null; - /// Gets or sets url to your application, used in the user agent. - public string AppUrl { get; set; } = null; - /// Gets or sets the version of your application, used in the user agent. - public string AppVersion { get; set; } = null; - - /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. - public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - - /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. - public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get; set; } = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; - - //Performance - - /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. - public int MessageCacheSize { get; set; } = 100; - /// - /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.ServerPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. - /// - public bool UsePermissionsCache { get; set; } = true; - /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. - public bool EnablePreUpdateEvents { get; set; } = true; - /// - /// Gets or sets the max number of users a server may have for offline users to be included in the READY packet. Max is 250. - /// Decreasing this may reduce CPU usage while increasing login time and network usage. - /// - public int LargeThreshold { get; set; } = 250; - - //Engines - - /// Gets or sets the REST engine to use.. Defaults to DefaultRestClientProvider, which uses .Net's HttpClient class. - public IRestClientProvider RestClientProvider { get; set; } = null; - /// - /// Gets or sets the WebSocket engine to use. Defaults to DefaultWebSocketProvider, which uses .Net's WebSocketClient class. - /// WebSockets are only used if DiscordClient.Connect() is called. - /// - public IWebSocketProvider WebSocketProvider { get; set; } = null; - } -} - diff --git a/ref/Entities/Channels/IPublicChannel.cs b/ref/Entities/Channels/IPublicChannel.cs deleted file mode 100644 index bd005a288..000000000 --- a/ref/Entities/Channels/IPublicChannel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IPublicChannel : IChannel - { - /// Gets the server this channel is a member of. - Server Server { get; } - /// Gets a collection of permission overwrites for this channel. - IEnumerable PermissionOverwrites { get; } - /// Gets the position of this public channel relative to others of the same type. - int Position { get; } - - /// Gets a user in this channel with the given id. - new Task GetUser(ulong id); - /// Gets a collection of all users in this channel. - new Task> GetUsers(); - - /// Gets the permission overwrite for a specific user, or null if one does not exist. - OverwritePermissions? GetPermissionOverwrite(ServerUser user); - /// Gets the permission overwrite for a specific role, or null if one does not exist. - OverwritePermissions? GetPermissionOverwrite(Role role); - /// Downloads a collection of all invites to this server. - Task> GetInvites(); - - /// Adds or updates the permission overwrite for the given user. - Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions); - /// Adds or updates the permission overwrite for the given role. - Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions); - /// Removes the permission overwrite for the given user, if one exists. - Task RemovePermissionOverwrite(ServerUser user); - /// Removes the permission overwrite for the given role, if one exists. - Task RemovePermissionOverwrite(Role role); - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false); - } -} diff --git a/ref/Entities/Channels/PrivateChannel.cs b/ref/Entities/Channels/PrivateChannel.cs deleted file mode 100644 index ee72c0828..000000000 --- a/ref/Entities/Channels/PrivateChannel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class PrivateChannel : ITextChannel, IChannel - { - /// - public DiscordClient Discord { get; } - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// - public PrivateUser Recipient { get; } - /// - public PrivateUser CurrentUser { get; } - - /// - ChannelType IChannel.Type => ChannelType.Private | ChannelType.Text; - /// - public string Name { get; } - - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task GetMessage(ulong id) => null; - /// - public Task> GetMessages(int limit = 100) => null; - /// - public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) => null; - - /// - public Task SendMessage(string text, bool isTTS = false) => null; - /// - public Task SendFile(string filePath, string text = null, bool isTTS = false) => null; - /// - public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) => null; - - /// - public Task SendIsTyping() => null; - - /// - public Task Update() => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Channels/TextChannel.cs b/ref/Entities/Channels/TextChannel.cs deleted file mode 100644 index 0b1b81c77..000000000 --- a/ref/Entities/Channels/TextChannel.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class TextChannel : ITextChannel, IMentionable, IModifiable - { - public sealed class Properties - { - public string Name { get; } - public string Topic { get; } - public int Position { get; } - } - - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// - public Server Server { get; } - - /// - public DiscordClient Discord { get; } - /// - public ChannelType Type => ChannelType.Public | ChannelType.Text; - - /// - public string Name { get; } - /// - public string Topic { get; } - /// - public int Position { get; } - - /// - public string Mention { get; } - /// - public IEnumerable PermissionOverwrites { get; } - - /// - public OverwritePermissions? GetPermissionOverwrite(ServerUser user) => null; - /// - public OverwritePermissions? GetPermissionOverwrite(Role role) => null; - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task GetMessage(ulong id) => null; - /// - public Task> GetMessages(int limit = 100) => null; - /// - public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) => null; - /// - public Task> GetInvites() => null; - - /// - public Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions) => null; - /// - public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) => null; - /// - public Task RemovePermissionOverwrite(ServerUser user) => null; - /// - public Task RemovePermissionOverwrite(Role role) => null; - - /// - public Task SendMessage(string text, bool isTTS = false) => null; - /// - public Task SendFile(string filePath, string text = null, bool isTTS = false) => null; - /// - public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) => null; - - /// - public Task SendIsTyping() => null; - - /// - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - - /// - public Task Update() => null; - /// - public Task Modify(Action func) => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Channels/VoiceChannel.cs b/ref/Entities/Channels/VoiceChannel.cs deleted file mode 100644 index 6552fadd7..000000000 --- a/ref/Entities/Channels/VoiceChannel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class VoiceChannel : IPublicChannel, IModifiable - { - public sealed class Properties - { - public string Name { get; } - public int Bitrate { get; set; } - public int Position { get; } - } - - /// - public ulong Id { get; } - /// - public EntityState State { get; } - /// - public Server Server { get; } - - /// - public DiscordClient Discord { get; } - /// - ChannelType IChannel.Type => ChannelType.Public | ChannelType.Voice; - - /// - public string Name { get; } - /// - public int Position { get; } - /// - public int Bitrate { get; } - - /// - public string Mention { get; } - /// - public IEnumerable PermissionOverwrites { get; } - - /// - public OverwritePermissions? GetPermissionOverwrite(ServerUser user) => null; - /// - public OverwritePermissions? GetPermissionOverwrite(Role role) => null; - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task> GetInvites() => null; - - /// - public Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions) => null; - /// - public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) => null; - /// - public Task RemovePermissionOverwrite(ServerUser user) => null; - /// - public Task RemovePermissionOverwrite(Role role) => null; - - /// - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - - /// - public Task Update() => null; - /// - public Task Modify(Action func) => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Color.cs b/ref/Entities/Color.cs deleted file mode 100644 index b3c78debf..000000000 --- a/ref/Entities/Color.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Discord -{ - public class Color - { - public static readonly Color Default = new Color(0); - - public uint RawValue { get; } - - public Color(uint rawValue) { } - public Color(byte r, byte g, byte b) { } - public Color(float r, float g, float b) { } - - public byte R { get; } - public byte G { get; } - public byte B { get; } - } -} diff --git a/ref/Entities/IModifiable.cs b/ref/Entities/IModifiable.cs deleted file mode 100644 index f264c96f2..000000000 --- a/ref/Entities/IModifiable.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IModifiable - { - /// Modifies one or more of the properties of this object. - Task Modify(Action func); - } -} diff --git a/ref/Entities/Invite/BasicInvite.cs b/ref/Entities/Invite/BasicInvite.cs deleted file mode 100644 index 37cd1704d..000000000 --- a/ref/Entities/Invite/BasicInvite.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public class BasicInvite : IEntity - { - public class TargetInfo - { - public ulong Id { get; } - public string Name { get; } - } - public class InviterInfo - { - public ulong Id { get; } - public string Name { get; } - public ushort Discriminator { get; } - public string AvatarId { get; } - public string AvatarUrl { get; } - } - - string IEntity.Id => Code; - public DiscordClient Discord { get; } - public EntityState State { get; } - - public string Code { get; } - public string XkcdCode { get; } - - public TargetInfo Server { get; } - public TargetInfo Channel { get; } - - public string Url { get; } - - public Task Accept() => null; - - public virtual Task Update() => null; - } -} diff --git a/ref/Entities/Invite/Invite.cs b/ref/Entities/Invite/Invite.cs deleted file mode 100644 index 11fead2af..000000000 --- a/ref/Entities/Invite/Invite.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public class Invite : BasicInvite - { - public int? MaxAge { get; } - public int Uses { get; } - public int? MaxUses { get; } - public bool IsRevoked { get; } - public bool IsTemporary { get; } - public DateTime CreatedAt { get; } - - public override Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Message.cs b/ref/Entities/Message.cs deleted file mode 100644 index 78c4e41bd..000000000 --- a/ref/Entities/Message.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Message : IEntity - { - public class Attachment : File - { - public string Id { get; } - public int Size { get; } - public string Filename { get; } - } - - public class Embed - { - public string Url { get; } - public string Type { get; } - public string Title { get; } - public string Description { get; } - public EmbedLink Author { get; } - public EmbedLink Provider { get; } - public File Thumbnail { get; } - public File Video { get; } - } - - public class EmbedLink - { - public string Url { get; } - public string Name { get; } - } - - public class File - { - public string Url { get; } - public string ProxyUrl { get; } - public int? Width { get; } - public int? Height { get; } - } - - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public ITextChannel Channel { get; } - public IUser User { get; } - public bool IsTTS { get; } - public string RawText { get; } - public string Text { get; } - public DateTime Timestamp { get; } - public DateTime? EditedTimestamp { get; } - public Attachment[] Attachments { get; } - public Embed[] Embeds { get; } - - public IReadOnlyList MentionedUsers { get; } - public IReadOnlyList MentionedChannels { get; } - public IReadOnlyList MentionedRoles { get; } - - public Server Server => null; - public bool IsAuthor => false; - - public bool IsMentioningMe(bool includeRoles = false) => false; - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Permissions/ChannelPermissions.cs b/ref/Entities/Permissions/ChannelPermissions.cs deleted file mode 100644 index d01f0430e..000000000 --- a/ref/Entities/Permissions/ChannelPermissions.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Discord -{ - public struct ChannelPermissions - { - public static ChannelPermissions None { get; } - public static ChannelPermissions TextOnly { get; } - public static ChannelPermissions PrivateOnly { get; } - public static ChannelPermissions VoiceOnly { get; } - public static ChannelPermissions All(ChannelType channelType) => default(ChannelPermissions); - - public uint RawValue { get; } - - public bool CreateInstantInvite { get; } - public bool ManagePermission { get; } - public bool ManageChannel { get; } - - public bool ReadMessages { get; } - public bool SendMessages { get; } - public bool SendTTSMessages { get; } - public bool ManageMessages { get; } - public bool EmbedLinks { get; } - public bool AttachFiles { get; } - public bool ReadMessageHistory { get; } - public bool MentionEveryone { get; } - - public bool Connect { get; } - public bool Speak { get; } - public bool MuteMembers { get; } - public bool DeafenMembers { get; } - public bool MoveMembers { get; } - public bool UseVoiceActivation { get; } - - public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this() - { - } - public ChannelPermissions(uint rawValue) - : this() - { - } - - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - => default(ChannelPermissions); - } -} diff --git a/ref/Entities/Permissions/OverwritePermissions.cs b/ref/Entities/Permissions/OverwritePermissions.cs deleted file mode 100644 index 1cda173ec..000000000 --- a/ref/Entities/Permissions/OverwritePermissions.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Discord -{ - public struct OverwritePermissions - { - public static OverwritePermissions InheritAll { get; } - - public uint AllowValue { get; } - public uint DenyValue { get; } - - public PermValue CreateInstantInvite { get; } - public PermValue ManagePermissions { get; } - public PermValue ManageChannel { get; } - public PermValue ReadMessages { get; } - public PermValue SendMessages { get; } - public PermValue SendTTSMessages { get; } - public PermValue ManageMessages { get; } - public PermValue EmbedLinks { get; } - public PermValue AttachFiles { get; } - public PermValue ReadMessageHistory { get; } - public PermValue MentionEveryone { get; } - - public PermValue Connect { get; } - public PermValue Speak { get; } - public PermValue MuteMembers { get; } - public PermValue DeafenMembers { get; } - public PermValue MoveMembers { get; } - public PermValue UseVoiceActivation { get; } - - public OverwritePermissions(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - : this() - { - } - - public OverwritePermissions(uint allow = 0, uint deny = 0) - : this() - { - } - - public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - => default(OverwritePermissions); - } -} diff --git a/ref/Entities/Permissions/PermissionOverwriteEntry.cs b/ref/Entities/Permissions/PermissionOverwriteEntry.cs deleted file mode 100644 index bbc11fba8..000000000 --- a/ref/Entities/Permissions/PermissionOverwriteEntry.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public struct PermissionOverwriteEntry - { - public PermissionTarget TargetType { get; } - public ulong TargetId { get; } - public OverwritePermissions Permissions { get; } - } -} diff --git a/ref/Entities/Permissions/ServerPermissions.cs b/ref/Entities/Permissions/ServerPermissions.cs deleted file mode 100644 index fe85c07dd..000000000 --- a/ref/Entities/Permissions/ServerPermissions.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace Discord -{ - public struct ServerPermissions - { - public static ServerPermissions None { get; } - public static ServerPermissions All { get; } - - public uint RawValue { get; } - - public bool CreateInstantInvite { get; } - public bool BanMembers { get; } - public bool KickMembers { get; } - public bool ManageRoles { get; } - public bool ManageChannels { get; } - public bool ManageServer { get; } - - public bool ReadMessages { get; } - public bool SendMessages { get; } - public bool SendTTSMessages { get; } - public bool ManageMessages { get; } - public bool EmbedLinks { get; } - public bool AttachFiles { get; } - public bool ReadMessageHistory { get; } - public bool MentionEveryone { get; } - - public bool Connect { get; } - public bool Speak { get; } - public bool MuteMembers { get; } - public bool DeafenMembers { get; } - public bool MoveMembers { get; } - public bool UseVoiceActivation { get; } - - public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this() - { - } - public ServerPermissions(uint rawValue) - : this() - { - } - - public ServerPermissions Modify(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - => default(ServerPermissions); - } -} diff --git a/ref/Entities/Profile.cs b/ref/Entities/Profile.cs deleted file mode 100644 index aa61e51b2..000000000 --- a/ref/Entities/Profile.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public class Profile : IEntity - { - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public string AvatarId { get; } - public string AvatarUrl { get; } - public ushort Discriminator { get; } - public string CurrentGame { get; } - public UserStatus Status { get; } - public string Mention { get; } - public string Email { get; } - public bool? IsVerified { get; } - - public string Name { get; set; } - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Region.cs b/ref/Entities/Region.cs deleted file mode 100644 index fbb801eaa..000000000 --- a/ref/Entities/Region.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Discord -{ - public class Region - { - public string Id { get; } - public string Name { get; } - public string Hostname { get; } - public int Port { get; } - public bool Vip { get; } - } -} diff --git a/ref/Entities/Role.cs b/ref/Entities/Role.cs deleted file mode 100644 index f5155db2e..000000000 --- a/ref/Entities/Role.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Role : IEntity, IMentionable - { - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public Server Server { get; } - - public string Name { get; } - public bool IsHoisted { get; } - public int Position { get; } - public bool IsManaged { get; } - public ServerPermissions Permissions { get; } - public Color Color { get; } - - public bool IsEveryone { get; } - public IEnumerable Members { get; } - - public string Mention { get; } - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Server.cs b/ref/Entities/Server.cs deleted file mode 100644 index a9078cb4b..000000000 --- a/ref/Entities/Server.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Server : IEntity - { - public class Emoji - { - public string Id { get; } - public string Name { get; } - public bool IsManaged { get; } - public bool RequireColons { get; } - public IEnumerable Roles { get; } - } - - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public ServerUser CurrentUser { get; } - public string IconId { get; } - public string SplashId { get; } - public string IconUrl { get; } - public string SplashUrl { get; } - public int ChannelCount { get; } - public int UserCount { get; } - public int RoleCount { get; } - public TextChannel DefaultChannel { get; } - public Role EveryoneRole { get; } - public IEnumerable Features { get; } - public IEnumerable CustomEmojis { get; } - public IEnumerable Channels { get; } - public IEnumerable TextChannels { get; } - public IEnumerable VoiceChannels { get; } - public IEnumerable Users { get; } - public IEnumerable Roles { get; } - - public string Name { get; set; } - public Region Region { get; set; } - public int AFKTimeout { get; set; } - public DateTime JoinedAt { get; set; } - public ServerUser Owner { get; set; } - public VoiceChannel AFKChannel { get; set; } - - public Task GetChannel(ulong id) => null; - public Task GetChannel(string mention) => null; - public Task GetRole(ulong id) => null; - public Task GetUser(ulong id) => null; - public Task GetUser(string name, ushort discriminator) => null; - public Task GetUser(string mention) => null; - public Task> GetBans() => null; - public Task> GetInvites() => null; - - public Task CreateTextChannel(string name) => null; - public Task CreateVoiceChannel(string name) => null; - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - public Task CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false) => null; - - public Task PruneUsers(int days = 30, bool simulate = false) => null; - - public Task Update() => null; - public Task Leave() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Users/IUser.cs b/ref/Entities/Users/IUser.cs deleted file mode 100644 index 02dd2d85b..000000000 --- a/ref/Entities/Users/IUser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public interface IUser : IEntity, IMentionable - { - bool IsPrivate { get; } - - string Name { get; } - ushort Discriminator { get; } - bool IsBot { get; } - string AvatarId { get; } - string AvatarUrl { get; } - string CurrentGame { get; } - UserStatus Status { get; } - - Task GetPrivateChannel(); - } -} diff --git a/ref/Entities/Users/PrivateUser.cs b/ref/Entities/Users/PrivateUser.cs deleted file mode 100644 index a6cc9d6e7..000000000 --- a/ref/Entities/Users/PrivateUser.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - //TODO: Should this be linked directly to the Profile when it represents us, instead of maintaining a cache of values? - public class PrivateUser : IUser - { - /// - public EntityState State { get; internal set; } - /// - public ulong Id { get; } - /// Returns the private channel for this user. - public PrivateChannel Channel { get; } - - /// - bool IUser.IsPrivate => true; - - /// - public string Name { get; } - /// - public ushort Discriminator { get; } - /// - public bool IsBot { get; } - /// - public string AvatarId { get; } - /// - public string CurrentGame { get; } - /// - public UserStatus Status { get; } - - /// - public DiscordClient Discord => Channel.Discord; - /// - public string AvatarUrl { get; } - /// - public string Mention { get; } - - /// - Task IUser.GetPrivateChannel() => Task.FromResult(Channel); - - public Task Update() => null; - } -} diff --git a/ref/Entities/Users/ServerUser.cs b/ref/Entities/Users/ServerUser.cs deleted file mode 100644 index 4ff86f67a..000000000 --- a/ref/Entities/Users/ServerUser.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class ServerUser : IUser - { - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// Returns the private channel for this user. - public Server Server { get; } - - /// - bool IUser.IsPrivate => false; - - /// - public string Name { get; } - /// - public ushort Discriminator { get; } - /// - public bool IsBot { get; } - /// - public string AvatarId { get; } - /// - public string CurrentGame { get; } - /// - public UserStatus Status { get; } - /// - public DateTime JoinedAt { get; } - /// - public IReadOnlyList Roles { get; } - - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted { get; } - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened { get; } - /// Returns true if the server is blocking audio from this user. - public bool IsServerMuted { get; } - /// Returns true if the server is blocking audio to this user. - public bool IsServerDeafened { get; } - /// Returns true if the server is temporarily blocking audio to/from this user. - public bool IsServerSuppressed { get; } - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel { get; } - - /// - public DiscordClient Discord { get; } - /// - public string AvatarUrl { get; } - /// - public string Mention { get; } - - public ServerPermissions ServerPermissions { get; } - - public ChannelPermissions GetPermissions(IPublicChannel channel) => default(ChannelPermissions); - /// - public Task GetPrivateChannel() => null; - public Task> GetChannels() => null; - - public bool HasRole(Role role) => false; - - public Task AddRoles(params Role[] roles) => null; - public Task RemoveRoles(params Role[] roles) => null; - - public Task Update() => null; - public Task Kick() => null; - public Task Ban(int pruneDays = 0) => null; - public Task Unban() => null; - } -} \ No newline at end of file diff --git a/ref/Enums/ChannelType.cs b/ref/Enums/ChannelType.cs deleted file mode 100644 index 5ebbf3aa6..000000000 --- a/ref/Enums/ChannelType.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord -{ - [Flags] - public enum ChannelType : byte - { - Public = 0x01, - Private = 0x02, - Text = 0x10, - Voice = 0x20 - } -} diff --git a/ref/Enums/ConnectionState.cs b/ref/Enums/ConnectionState.cs deleted file mode 100644 index dfd4ac9eb..000000000 --- a/ref/Enums/ConnectionState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Discord -{ - public enum ConnectionState - { - Disconnected, - Connecting, - Connected, - Disconnecting - } -} diff --git a/ref/Enums/EntityState.cs b/ref/Enums/EntityState.cs deleted file mode 100644 index 6ae71e4a3..000000000 --- a/ref/Enums/EntityState.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Discord -{ - public enum EntityState : byte - { - /// Object is not attached to a cache manager nor receiving live updates. - Detached = 0, - /// Object is attached to a cache manager and receiving live updates. - Attached, - /// Object was deleted. - Deleted, - /// Object is currently waiting to be created. - Queued, - /// Object's creation was aborted. - Aborted, - /// Object's creation failed. - Failed - } -} diff --git a/ref/Enums/ImageType.cs b/ref/Enums/ImageType.cs deleted file mode 100644 index 738c67a3d..000000000 --- a/ref/Enums/ImageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum ImageType - { - None, - Jpeg, - Png - } -} diff --git a/ref/Enums/LogSeverity.cs b/ref/Enums/LogSeverity.cs deleted file mode 100644 index 785b0ef46..000000000 --- a/ref/Enums/LogSeverity.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Discord -{ - public enum LogSeverity - { - Critical = 0, - Error = 1, - Warning = 2, - Info = 3, - Verbose = 4, - Debug = 5 - } -} diff --git a/ref/Enums/PermValue.cs b/ref/Enums/PermValue.cs deleted file mode 100644 index fe048b016..000000000 --- a/ref/Enums/PermValue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum PermValue - { - Allow, - Deny, - Inherit - } -} diff --git a/ref/Enums/PermissionTarget.cs b/ref/Enums/PermissionTarget.cs deleted file mode 100644 index 96595fb69..000000000 --- a/ref/Enums/PermissionTarget.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum PermissionTarget - { - Role, - User - } -} diff --git a/ref/Enums/Relative.cs b/ref/Enums/Relative.cs deleted file mode 100644 index aade047d1..000000000 --- a/ref/Enums/Relative.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum Relative - { - Before, - After - } -} diff --git a/ref/Enums/UserStatus.cs b/ref/Enums/UserStatus.cs deleted file mode 100644 index f2fdfda7c..000000000 --- a/ref/Enums/UserStatus.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum UserStatus - { - Online, - Idle, - Offline - } -} diff --git a/ref/Events/ChannelEventArgs.cs b/ref/Events/ChannelEventArgs.cs deleted file mode 100644 index 583075e08..000000000 --- a/ref/Events/ChannelEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord -{ - public class ChannelEventArgs : EventArgs - { - public IChannel Channel => null; - } -} diff --git a/ref/Events/ChannelUpdatedEventArgs.cs b/ref/Events/ChannelUpdatedEventArgs.cs deleted file mode 100644 index bcd809521..000000000 --- a/ref/Events/ChannelUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ChannelUpdatedEventArgs : EventArgs - { - public IChannel Before => null; - public IChannel After => null; - } -} diff --git a/ref/Events/DisconnectedEventArgs.cs b/ref/Events/DisconnectedEventArgs.cs deleted file mode 100644 index 616f3f09d..000000000 --- a/ref/Events/DisconnectedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class DisconnectedEventArgs : EventArgs - { - public bool WasUnexpected => false; - public Exception Exception => null; - } -} diff --git a/ref/Events/LogMessageEventArgs.cs b/ref/Events/LogMessageEventArgs.cs deleted file mode 100644 index 7dec182d1..000000000 --- a/ref/Events/LogMessageEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord -{ - public class LogMessageEventArgs : EventArgs - { - public LogSeverity Severity => default(LogSeverity); - public string Source => null; - public string Message => null; - public Exception Exception => null; - } -} diff --git a/ref/Events/MessageEventArgs.cs b/ref/Events/MessageEventArgs.cs deleted file mode 100644 index f75c7f1a8..000000000 --- a/ref/Events/MessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class MessageEventArgs : EventArgs - { - public Message Message => null; - public IUser User => null; - public ITextChannel Channel => null; - } -} diff --git a/ref/Events/MessageUpdatedEventArgs.cs b/ref/Events/MessageUpdatedEventArgs.cs deleted file mode 100644 index d323bf809..000000000 --- a/ref/Events/MessageUpdatedEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord -{ - public class MessageUpdatedEventArgs : EventArgs - { - public Message Before => null; - public Message After => null; - public IUser User => null; - public ITextChannel Channel => null; - } -} diff --git a/ref/Events/ProfileUpdatedEventArgs.cs b/ref/Events/ProfileUpdatedEventArgs.cs deleted file mode 100644 index dba55af3b..000000000 --- a/ref/Events/ProfileUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ProfileUpdatedEventArgs : EventArgs - { - public Profile Before => null; - public Profile After => null; - } -} diff --git a/ref/Events/RoleEventArgs.cs b/ref/Events/RoleEventArgs.cs deleted file mode 100644 index db1d09cbc..000000000 --- a/ref/Events/RoleEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class RoleEventArgs : EventArgs - { - public Role Role => null; - public Server Server => null; - } -} diff --git a/ref/Events/RoleUpdatedEventArgs.cs b/ref/Events/RoleUpdatedEventArgs.cs deleted file mode 100644 index 1fa0f2a81..000000000 --- a/ref/Events/RoleUpdatedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class RoleUpdatedEventArgs : EventArgs - { - public Role Before => null; - public Role After => null; - public Server Server => null; - } -} diff --git a/ref/Events/ServerEventArgs.cs b/ref/Events/ServerEventArgs.cs deleted file mode 100644 index b06993de9..000000000 --- a/ref/Events/ServerEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerEventArgs : EventArgs - { - public Server Server => null; - } -} diff --git a/ref/Events/ServerUpdatedEventArgs.cs b/ref/Events/ServerUpdatedEventArgs.cs deleted file mode 100644 index 1e05f1721..000000000 --- a/ref/Events/ServerUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerUpdatedEventArgs : EventArgs - { - public Server Before => null; - public Server After => null; - } -} diff --git a/ref/Events/TypingEventArgs.cs b/ref/Events/TypingEventArgs.cs deleted file mode 100644 index f45313687..000000000 --- a/ref/Events/TypingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public class TypingEventArgs - { - public ITextChannel Channel { get; } - public IUser User { get; } - - public TypingEventArgs(ITextChannel channel, IUser user) - { - Channel = channel; - User = user; - } - } -} diff --git a/ref/Events/UserEventArgs.cs b/ref/Events/UserEventArgs.cs deleted file mode 100644 index f1cce29fc..000000000 --- a/ref/Events/UserEventArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -namespace Discord -{ - public class UserEventArgs : EventArgs - { - public IUser User => null; - } -} diff --git a/ref/Events/UserUpdatedEventArgs.cs b/ref/Events/UserUpdatedEventArgs.cs deleted file mode 100644 index c45c60701..000000000 --- a/ref/Events/UserUpdatedEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -namespace Discord -{ - public class UserUpdatedEventArgs : EventArgs - { - public IUser Before => null; - public IUser After => null; - } -} diff --git a/ref/Format.cs b/ref/Format.cs deleted file mode 100644 index e30931ae9..000000000 --- a/ref/Format.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public static class Format - { - public static string Escape(string text) => null; - - public static string Bold(string text, bool escape = true) => null; - public static string Italics(string text, bool escape = true) => null; - public static string Underline(string text, bool escape = true) => null; - public static string Strikeout(string text, bool escape = true) => null; - - public static string Code(string text, string language = null) => null; - } -} diff --git a/ref/ILogger.cs b/ref/ILogger.cs deleted file mode 100644 index a3123edc9..000000000 --- a/ref/ILogger.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace Discord.Logging -{ - public interface ILogger - { - LogSeverity Level { get; } - - void Log(LogSeverity severity, string message, Exception exception = null); - void Error(string message, Exception exception = null); - void Error(Exception exception); - void Warning(string message, Exception exception = null); - void Warning(Exception exception); - void Info(string message, Exception exception = null); - void Info(Exception exception); - void Verbose(string message, Exception exception = null); - void Verbose(Exception exception); - void Debug(string message, Exception exception = null); - void Debug(Exception exception); - -#if DOTNET5_4 - void Log(LogSeverity severity, FormattableString message, Exception exception = null); - void Error(FormattableString message, Exception exception = null); - void Warning(FormattableString message, Exception exception = null); - void Info(FormattableString message, Exception exception = null); - void Verbose(FormattableString message, Exception exception = null); - void Debug(FormattableString message, Exception exception = null); -#endif - } -} diff --git a/ref/MessageQueue.cs b/ref/MessageQueue.cs deleted file mode 100644 index 5f56abd1e..000000000 --- a/ref/MessageQueue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public class MessageQueue - { - public int Count { get; } - - public void Clear() { } - } -} diff --git a/ref/Net/HttpException.cs b/ref/Net/HttpException.cs deleted file mode 100644 index 3704ffb83..000000000 --- a/ref/Net/HttpException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Net; - -namespace Discord.Net -{ - public class HttpException : Exception - { - public HttpStatusCode StatusCode { get; } - - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {(int)statusCode} ({statusCode})") - { - StatusCode = statusCode; - } - } -} diff --git a/ref/Net/Rest/CompletedRequestEventArgs.cs b/ref/Net/Rest/CompletedRequestEventArgs.cs deleted file mode 100644 index ed9d1673f..000000000 --- a/ref/Net/Rest/CompletedRequestEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Discord.Net.Rest -{ - public class CompletedRequestEventArgs : RequestEventArgs - { - public object Response { get; set; } - public string ResponseJson { get; set; } - public double Milliseconds { get; set; } - - public CompletedRequestEventArgs(IRestRequest request, object response, string responseJson, double milliseconds) - : base(request) - { - Response = response; - ResponseJson = responseJson; - Milliseconds = milliseconds; - } - } -} diff --git a/ref/Net/Rest/IRestClient.cs b/ref/Net/Rest/IRestClient.cs deleted file mode 100644 index 83c0405c7..000000000 --- a/ref/Net/Rest/IRestClient.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Rest -{ - public interface IRestClient - { - event EventHandler SendingRequest; - event EventHandler SentRequest; - - CancellationToken CancelToken { get; } - string Token { get; } - - Task Send(IRestRequest request) - where ResponseT : class; - Task Send(IRestRequest request); - - Task Send(IRestFileRequest request) - where ResponseT : class; - Task Send(IRestFileRequest request); - } -} diff --git a/ref/Net/Rest/IRestClientProvider.cs b/ref/Net/Rest/IRestClientProvider.cs deleted file mode 100644 index cb22a7474..000000000 --- a/ref/Net/Rest/IRestClientProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace Discord.Net.Rest -{ - public interface IRestClientProvider - { - IRestClient Create(string baseUrl, CancellationToken cancelToken); - } -} diff --git a/ref/Net/Rest/RequestEventArgs.cs b/ref/Net/Rest/RequestEventArgs.cs deleted file mode 100644 index cac734fc6..000000000 --- a/ref/Net/Rest/RequestEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord.Net.Rest -{ - public class RequestEventArgs : EventArgs - { - public IRestRequest Request { get; set; } - public bool Cancel { get; set; } - - public RequestEventArgs(IRestRequest request) { } - } -} diff --git a/ref/Net/TimeoutException.cs b/ref/Net/TimeoutException.cs deleted file mode 100644 index d1a644049..000000000 --- a/ref/Net/TimeoutException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class TimeoutException : OperationCanceledException - { - public TimeoutException() { } - } -} diff --git a/ref/Net/WebSocketException.cs b/ref/Net/WebSocketException.cs deleted file mode 100644 index df6377e13..000000000 --- a/ref/Net/WebSocketException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class WebSocketException : Exception - { - public int Code { get; } - public string Reason { get; } - - public WebSocketException(int code, string reason) { } - } -} diff --git a/ref/Net/WebSockets/BinaryMessageEventArgs.cs b/ref/Net/WebSockets/BinaryMessageEventArgs.cs deleted file mode 100644 index 3fd4425fa..000000000 --- a/ref/Net/WebSockets/BinaryMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class BinaryMessageEventArgs : EventArgs - { - public byte[] Data { get; } - - public BinaryMessageEventArgs(byte[] data) { } - } -} diff --git a/ref/Net/WebSockets/GatewaySocket.cs b/ref/Net/WebSockets/GatewaySocket.cs deleted file mode 100644 index e8f2ddd3d..000000000 --- a/ref/Net/WebSockets/GatewaySocket.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Discord.Net.Rest; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public class GatewaySocket - { - public string SessionId { get; } - - public event EventHandler ReceivedDispatch = delegate { }; - - public Task Connect(IRestClient rest, CancellationToken parentCancelToken) => null; - public Task Disconnect() => null; - - public void SendIdentify(string token) { } - - public void SendResume() { } - public void SendHeartbeat() { } - public void SendUpdateStatus(long? idleSince, string gameName) { } - public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) { } - public void SendRequestMembers(IEnumerable serverId, string query, int limit) { } - - public void WaitForConnection(CancellationToken cancelToken) { } - } -} diff --git a/ref/Net/WebSockets/IWebSocket.cs b/ref/Net/WebSockets/IWebSocket.cs deleted file mode 100644 index 06a274305..000000000 --- a/ref/Net/WebSockets/IWebSocket.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocket - { - CancellationToken CancelToken { get; } - ConnectionState State { get; } - string Host { get; set; } - - event EventHandler Connected; - event EventHandler Disconnected; - } -} diff --git a/ref/Net/WebSockets/IWebSocketEngine.cs b/ref/Net/WebSockets/IWebSocketEngine.cs deleted file mode 100644 index 68f31f12b..000000000 --- a/ref/Net/WebSockets/IWebSocketEngine.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocketEngine - { - event EventHandler BinaryMessage; - event EventHandler TextMessage; - - Task Connect(string host, CancellationToken cancelToken); - Task Disconnect(); - void QueueMessage(string message); - IEnumerable GetTasks(CancellationToken cancelToken); - } -} diff --git a/ref/Net/WebSockets/IWebSocketProvider.cs b/ref/Net/WebSockets/IWebSocketProvider.cs deleted file mode 100644 index 20f7559be..000000000 --- a/ref/Net/WebSockets/IWebSocketProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocketProvider - { - IWebSocket Create(CancellationToken cancelToken); - } -} diff --git a/ref/Net/WebSockets/TextMessageEventArgs.cs b/ref/Net/WebSockets/TextMessageEventArgs.cs deleted file mode 100644 index e4e186044..000000000 --- a/ref/Net/WebSockets/TextMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class TextMessageEventArgs : EventArgs - { - public string Message { get; } - - public TextMessageEventArgs(string msg) { Message = msg; } - } -} diff --git a/ref/Net/WebSockets/WebSocketEventEventArgs.cs b/ref/Net/WebSockets/WebSocketEventEventArgs.cs deleted file mode 100644 index 676c0ba6e..000000000 --- a/ref/Net/WebSockets/WebSocketEventEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; - -namespace Discord.Net.WebSockets -{ - public class WebSocketEventEventArgs : EventArgs - { - public string Type { get; } - public JToken Payload { get; } - } -} diff --git a/ref/project.json b/ref/project.json deleted file mode 100644 index 565bc2e86..000000000 --- a/ref/project.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "version": "0.9.0-rc3-3", - "description": "An unofficial .Net API wrapper for the Discord client.", - "authors": [ - "RogueException" - ], - "tags": [ - "discord", - "discordapp" - ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "allowUnsafe": true, - "warningsAsErrors": true - }, - - "configurations": { - "TestResponses": { - "compilationOptions": { - "define": [ - "DEBUG", - "TRACE", - "TEST_RESPONSES" - ] - } - } - }, - - "dependencies": { - "Newtonsoft.Json": "8.0.1", - "Nito.AsyncEx": "3.0.1" - }, - - "frameworks": { - "dotnet5.4": { - "dependencies": { - "System.Collections": "4.0.11-beta-23516", - "System.Collections.Concurrent": "4.0.11-beta-23516", - "System.Dynamic.Runtime": "4.0.11-beta-23516", - "System.IO.FileSystem": "4.0.1-beta-23516", - "System.IO.Compression": "4.1.0-beta-23516", - "System.Linq": "4.0.1-beta-23516", - "System.Net.Http": "4.0.1-beta-23516", - "System.Net.NameResolution": "4.0.0-beta-23516", - "System.Net.Sockets": "4.1.0-beta-23409", - "System.Net.Requests": "4.0.11-beta-23516", - "System.Net.WebSockets.Client": "4.0.0-beta-23516", - "System.Reflection": "4.1.0-beta-23516", - "System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", - "System.Runtime.InteropServices": "4.0.21-beta-23516", - "System.Runtime.Serialization.Primitives": "4.1.0-beta-23516", - "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", - "System.Text.RegularExpressions": "4.0.11-beta-23516", - "System.Threading": "4.0.11-beta-23516" - } - }, - "net45": { - "frameworkAssemblies": { - "System.Runtime": { - "type": "build", - "version": "" - }, - "System.Threading.Tasks": { - "type": "build", - "version": "" - } - }, - "dependencies": { - "WebSocket4Net": "0.14.1", - "RestSharp": "105.2.3" - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs deleted file mode 100644 index 882dca1fe..000000000 --- a/src/Discord.Net.Audio/AudioClient.cs +++ /dev/null @@ -1,283 +0,0 @@ -using Discord.API.Client.GatewaySocket; -using Discord.API.Client.Rest; -using Discord.Logging; -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using Newtonsoft.Json; -using Nito.AsyncEx; -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - internal class AudioClient : IAudioClient - { - private readonly DiscordConfig _config; - private readonly AsyncLock _connectionLock; - private readonly TaskManager _taskManager; - private ConnectionState _gatewayState; - - internal Logger Logger { get; } - - public int Id { get; } - public AudioService Service { get; } - public AudioServiceConfig Config { get; } - public RestClient ClientAPI { get; } - public GatewaySocket GatewaySocket { get; } - public VoiceSocket VoiceSocket { get; } - public JsonSerializer Serializer { get; } - - public CancellationToken CancelToken { get; private set; } - public string SessionId => GatewaySocket.SessionId; - - public ConnectionState State => VoiceSocket.State; - public Server Server => VoiceSocket.Server; - public VoiceChannel Channel => VoiceSocket.Channel; - - public AudioClient(DiscordClient client, Server server, int id) - { - Id = id; - Service = client.GetService(); - Config = Service.Config; - Serializer = client.Serializer; - _gatewayState = (int)ConnectionState.Disconnected; - - //Logging - Logger = client.Log.CreateLogger($"AudioClient #{id}"); - - //Async - _taskManager = new TaskManager(Cleanup, false); - _connectionLock = new AsyncLock(); - CancelToken = new CancellationToken(true); - - //Networking - if (Config.EnableMultiserver) - { - //TODO: We can remove this hack when official API launches - var baseConfig = client.Config; - var builder = new DiscordConfigBuilder - { - AppName = baseConfig.AppName, - AppUrl = baseConfig.AppUrl, - AppVersion = baseConfig.AppVersion, - CacheToken = baseConfig.CacheDir != null, - ConnectionTimeout = baseConfig.ConnectionTimeout, - EnablePreUpdateEvents = false, - FailedReconnectDelay = baseConfig.FailedReconnectDelay, - LargeThreshold = 1, - LogLevel = baseConfig.LogLevel, - MessageCacheSize = 0, - ReconnectDelay = baseConfig.ReconnectDelay, - UsePermissionsCache = false - }; - _config = builder.Build(); - - ClientAPI = new JsonRestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}")); - GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}")); - GatewaySocket.Connected += (s, e) => - { - if (_gatewayState == ConnectionState.Connecting) - EndGatewayConnect(); - }; - } - else - { - _config = client.Config; - GatewaySocket = client.GatewaySocket; - } - GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); - VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}")); - VoiceSocket.Server = server; - } - - public async Task Connect() - { - if (Config.EnableMultiserver) - await BeginGatewayConnect().ConfigureAwait(false); - else - { - var cancelSource = new CancellationTokenSource(); - CancelToken = cancelSource.Token; - await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); - } - } - private async Task BeginGatewayConnect() - { - try - { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - await Disconnect().ConfigureAwait(false); - _taskManager.ClearException(); - - ClientAPI.Token = Service.Client.ClientAPI.Token; - - Stopwatch stopwatch = null; - if (_config.LogLevel >= LogSeverity.Verbose) - stopwatch = Stopwatch.StartNew(); - _gatewayState = ConnectionState.Connecting; - - var cancelSource = new CancellationTokenSource(); - CancelToken = cancelSource.Token; - ClientAPI.CancelToken = CancelToken; - - await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); - - await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); - GatewaySocket.WaitForConnection(CancelToken); - - if (_config.LogLevel >= LogSeverity.Verbose) - { - stopwatch.Stop(); - double seconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2); - Logger.Verbose($"Connection took {seconds} sec"); - } - } - } - catch (Exception ex) - { - await _taskManager.SignalError(ex).ConfigureAwait(false); - throw; - } - } - private void EndGatewayConnect() - { - _gatewayState = ConnectionState.Connected; - } - - public async Task Disconnect() - { - await _taskManager.Stop(true).ConfigureAwait(false); - if (Config.EnableMultiserver) - ClientAPI.Token = null; - } - private async Task Cleanup() - { - var oldState = _gatewayState; - _gatewayState = ConnectionState.Disconnecting; - - if (Config.EnableMultiserver) - { - if (oldState == ConnectionState.Connected) - { - try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } - catch (OperationCanceledException) { } - } - - await GatewaySocket.Disconnect().ConfigureAwait(false); - ClientAPI.Token = null; - } - - var server = VoiceSocket.Server; - VoiceSocket.Server = null; - VoiceSocket.Channel = null; - if (Config.EnableMultiserver) - await Service.RemoveClient(server, this).ConfigureAwait(false); - SendVoiceUpdate(server.Id, null); - - await VoiceSocket.Disconnect().ConfigureAwait(false); - if (Config.EnableMultiserver) - await GatewaySocket.Disconnect().ConfigureAwait(false); - - _gatewayState = (int)ConnectionState.Disconnected; - } - - public async Task Join(VoiceChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (channel.Type != ChannelType.Voice) - throw new ArgumentException("Channel must be a voice channel.", nameof(channel)); - if (channel == VoiceSocket.Channel) return; - var server = channel.Server; - if (server != VoiceSocket.Server) - throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); - if (VoiceSocket.Server == null) - throw new InvalidOperationException("This client has been closed."); - - SendVoiceUpdate(channel.Server.Id, channel.Id); - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - await Task.Run(() => VoiceSocket.WaitForConnection(CancelToken)).ConfigureAwait(false); - } - - private async void OnReceivedEvent(WebSocketEventEventArgs e) - { - try - { - switch (e.Type) - { - case "VOICE_STATE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) - { - if (data.ChannelId == null) - await Disconnect().ConfigureAwait(false); - else - { - var channel = Service.Client.GetChannel(data.ChannelId.Value) as VoiceChannel; - if (channel != null) - VoiceSocket.Channel = channel; - else - { - Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); - await Disconnect().ConfigureAwait(false); - } - } - } - } - break; - case "VOICE_SERVER_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - if (data.GuildId == VoiceSocket.Server?.Id) - { - var client = Service.Client; - var id = client.CurrentUser?.Id; - if (id != null) - { - var host = "wss://" + e.Payload.Value("endpoint").Split(':')[0]; - await VoiceSocket.Connect(host, data.Token, id.Value, GatewaySocket.SessionId, CancelToken).ConfigureAwait(false); - } - } - } - break; - } - } - catch (Exception ex) - { - Logger.Error($"Error handling {e.Type} event", ex); - } - } - - public void Send(byte[] data, int offset, int count) - { - if (data == null) throw new ArgumentException(nameof(data)); - if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); - if (VoiceSocket.Server == null) return; //Has been closed - if (count == 0) return; - - VoiceSocket.SendPCMFrames(data, offset, count); - } - - public void Clear() - { - if (VoiceSocket.Server == null) return; //Has been closed - VoiceSocket.ClearPCMFrames(); - } - public void Wait() - { - if (VoiceSocket.Server == null) return; //Has been closed - VoiceSocket.WaitForQueue(); - } - - public void SendVoiceUpdate(ulong? serverId, ulong? channelId) - { - GatewaySocket.SendUpdateVoice(serverId, channelId, - (Service.Config.Mode | AudioMode.Outgoing) == 0, - (Service.Config.Mode | AudioMode.Incoming) == 0); - } - } -} diff --git a/src/Discord.Net.Audio/AudioExtensions.cs b/src/Discord.Net.Audio/AudioExtensions.cs deleted file mode 100644 index 7def445a6..000000000 --- a/src/Discord.Net.Audio/AudioExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - public static class AudioExtensions - { - public static DiscordClient UsingAudio(this DiscordClient client, AudioServiceConfig config = null) - { - client.AddService(new AudioService(config)); - return client; - } - public static DiscordClient UsingAudio(this DiscordClient client, Action configFunc = null) - { - var builder = new AudioServiceConfigBuilder(); - configFunc(builder); - client.AddService(new AudioService(builder)); - return client; - } - - public static Task JoinAudio(this VoiceChannel channel) => channel.Client.GetService().Join(channel); - public static Task LeaveAudio(this VoiceChannel channel) => channel.Client.GetService().Leave(channel); - public static Task LeaveAudio(this Server server) => server.Client.GetService().Leave(server); - public static IAudioClient GetAudioClient(this Server server) => server.Client.GetService().GetClient(server); - } -} diff --git a/src/Discord.Net.Audio/AudioMode.cs b/src/Discord.Net.Audio/AudioMode.cs deleted file mode 100644 index b9acdbf89..000000000 --- a/src/Discord.Net.Audio/AudioMode.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Audio -{ - public enum AudioMode : byte - { - Outgoing = 1, - Incoming = 2, - Both = Outgoing | Incoming - } -} diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs deleted file mode 100644 index e44a4a1ce..000000000 --- a/src/Discord.Net.Audio/AudioService.cs +++ /dev/null @@ -1,193 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - public class AudioService : IService - { - private readonly AsyncLock _asyncLock; - private AudioClient _defaultClient; //Only used for single server - private VirtualClient _currentClient; //Only used for single server - private ConcurrentDictionary _voiceClients; - private ConcurrentDictionary _talkingUsers; - private int _nextClientId; - - public DiscordClient Client { get; private set; } - public AudioServiceConfig Config { get; } - - public event EventHandler Connected = delegate { }; - public event EventHandler Disconnected = delegate { }; - public event EventHandler UserIsSpeakingUpdated = delegate { }; - - private void OnConnected() - => Connected(this, EventArgs.Empty); - private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) - => Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex)); - private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) - => UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking)); - - public AudioService() - : this(new AudioServiceConfigBuilder()) - { - } - public AudioService(AudioServiceConfigBuilder builder) - : this(builder.Build()) - { - } - public AudioService(AudioServiceConfig config) - { - Config = config; - _asyncLock = new AsyncLock(); - - } - void IService.Install(DiscordClient client) - { - Client = client; - - if (Config.EnableMultiserver) - _voiceClients = new ConcurrentDictionary(); - else - { - var logger = Client.Log.CreateLogger("Voice"); - _defaultClient = new AudioClient(Client, null, 0); - } - _talkingUsers = new ConcurrentDictionary(); - - client.GatewaySocket.Disconnected += async (s, e) => - { - if (Config.EnableMultiserver) - { - var tasks = _voiceClients - .Select(x => - { - var val = x.Value; - if (val != null) - return x.Value.Disconnect(); - else - return TaskHelper.CompletedTask; - }) - .ToArray(); - await Task.WhenAll(tasks).ConfigureAwait(false); - _voiceClients.Clear(); - } - foreach (var member in _talkingUsers) - { - bool ignored; - if (_talkingUsers.TryRemove(member.Key, out ignored)) - OnUserIsSpeakingUpdated(member.Key, false); - } - }; - } - - public IAudioClient GetClient(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (Config.EnableMultiserver) - { - AudioClient client; - if (_voiceClients.TryGetValue(server.Id, out client)) - return client; - else - return null; - } - else - { - if (server == _currentClient.Server) - return _currentClient; - else - return null; - } - } - - //Called from AudioClient.Disconnect - internal async Task RemoveClient(Server server, AudioClient client) - { - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - if (_voiceClients.TryUpdate(server.Id, null, client)) - _voiceClients.TryRemove(server.Id, out client); - } - } - - public async Task Join(VoiceChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - - var server = channel.Server; - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - if (Config.EnableMultiserver) - { - AudioClient client; - if (!_voiceClients.TryGetValue(server.Id, out client)) - { - client = new AudioClient(Client, server, unchecked(++_nextClientId)); - _voiceClients[server.Id] = client; - - await client.Connect().ConfigureAwait(false); - - /*voiceClient.VoiceSocket.FrameReceived += (s, e) => - { - OnFrameReceieved(e); - }; - voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => - { - var user = server.GetUser(e.UserId); - OnUserIsSpeakingUpdated(user, e.IsSpeaking); - };*/ - } - - await client.Join(channel).ConfigureAwait(false); - return client; - } - else - { - if (_defaultClient.Server != server) - { - await _defaultClient.Disconnect().ConfigureAwait(false); - _defaultClient.VoiceSocket.Server = server; - await _defaultClient.Connect().ConfigureAwait(false); - } - var client = new VirtualClient(_defaultClient, server); - _currentClient = client; - - await client.Join(channel).ConfigureAwait(false); - return client; - } - - } - } - - public Task Leave(Server server) => Leave(server, null); - public Task Leave(VoiceChannel channel) => Leave(channel.Server, channel); - private async Task Leave(Server server, VoiceChannel channel) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (Config.EnableMultiserver) - { - AudioClient client; - //Potential race condition if changing channels during this call, but that's acceptable - if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) - { - if (_voiceClients.TryRemove(server.Id, out client)) - await client.Disconnect().ConfigureAwait(false); - } - } - else - { - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - var client = GetClient(server) as VirtualClient; - if (client != null && client.Channel == channel) - await _defaultClient.Disconnect().ConfigureAwait(false); - } - } - - } - } -} diff --git a/src/Discord.Net.Audio/AudioServiceConfig.cs b/src/Discord.Net.Audio/AudioServiceConfig.cs deleted file mode 100644 index 89d05d85b..000000000 --- a/src/Discord.Net.Audio/AudioServiceConfig.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Discord.Audio -{ - public class AudioServiceConfigBuilder - { - /// Enables the voice websocket and UDP client and specifies how it will be used. - public AudioMode Mode { get; set; } = AudioMode.Outgoing; - - /// Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. - public bool EnableEncryption { get; set; } = true; - /// - /// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). - /// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly. - /// - public bool EnableMultiserver { get; set; } = false; - - /// Gets or sets the buffer length (in milliseconds) for outgoing voice packets. - public int BufferLength { get; set; } = 1000; - /// Gets or sets the bitrate used (in kbit/s, between 1 and MaxBitrate inclusively) for outgoing voice packets. A null value will use default Opus settings. - public int? Bitrate { get; set; } = null; - /// Gets or sets the number of channels (1 or 2) used in both input provided to IAudioClient and output send to Discord. Defaults to 2 (stereo). - public int Channels { get; set; } = 2; - - public AudioServiceConfig Build() => new AudioServiceConfig(this); - } - - public class AudioServiceConfig - { - public const int MaxBitrate = 128; - - public AudioMode Mode { get; } - - public bool EnableEncryption { get; } - public bool EnableMultiserver { get; } - - public int BufferLength { get; } - public int? Bitrate { get; } - public int Channels { get; } - - internal AudioServiceConfig(AudioServiceConfigBuilder builder) - { - Mode = builder.Mode; - - EnableEncryption = builder.EnableEncryption; - EnableMultiserver = builder.EnableMultiserver; - - BufferLength = builder.BufferLength; - Bitrate = builder.Bitrate; - Channels = builder.Channels; - } - } -} diff --git a/src/Discord.Net.Audio/Discord.Net.Audio.xproj b/src/Discord.Net.Audio/Discord.Net.Audio.xproj deleted file mode 100644 index 4eb480f88..000000000 --- a/src/Discord.Net.Audio/Discord.Net.Audio.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - dff7afe3-ca77-4109-bade-b4b49a4f6648 - Discord.Audio - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Audio/IAudioClient.cs b/src/Discord.Net.Audio/IAudioClient.cs deleted file mode 100644 index a986fad7d..000000000 --- a/src/Discord.Net.Audio/IAudioClient.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - public interface IAudioClient - { - /// Gets the unique identifier for this client. - int Id { get; } - /// Gets the session id for the current connection. - string SessionId { get; } - /// Gets the current state of this client. - ConnectionState State { get; } - /// Gets the channel this client is currently a member of. - VoiceChannel Channel { get; } - /// Gets the server this client is bound to. - Server Server { get; } - /// Gets a cancellation token that triggers when the client is manually disconnected. - CancellationToken CancelToken { get; } - - /// Gets the internal RestClient for the Client API endpoint. - RestClient ClientAPI { get; } - /// Gets the internal WebSocket for the Gateway event stream. - GatewaySocket GatewaySocket { get; } - /// Gets the internal WebSocket for the Voice control stream. - VoiceSocket VoiceSocket { get; } - - /// Moves the client to another channel on the same server. - Task Join(VoiceChannel channel); - /// Disconnects from the Discord server, canceling any pending requests. - Task Disconnect(); - - /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. - /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. - /// Offset . - /// Number of bytes in this frame. - void Send(byte[] data, int offset, int count); - /// Clears the PCM buffer. - void Clear(); - /// Blocks until the voice output buffer is empty. - void Wait(); - } -} diff --git a/src/Discord.Net.Audio/InternalFrameEventArgs.cs b/src/Discord.Net.Audio/InternalFrameEventArgs.cs deleted file mode 100644 index b74dc8295..000000000 --- a/src/Discord.Net.Audio/InternalFrameEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Discord -{ - internal class InternalFrameEventArgs : EventArgs - { - public ulong UserId { get; } - public ulong ChannelId { get; } - public byte[] Buffer { get; } - public int Offset { get; } - public int Count { get; } - - public InternalFrameEventArgs(ulong userId, ulong channelId, byte[] buffer, int offset, int count) - { - UserId = userId; - ChannelId = channelId; - Buffer = buffer; - Offset = offset; - Count = count; - } - } -} diff --git a/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs b/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs deleted file mode 100644 index 641e863f4..000000000 --- a/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.Audio -{ - internal class InternalIsSpeakingEventArgs - { - public ulong UserId { get; } - public bool IsSpeaking { get; } - - public InternalIsSpeakingEventArgs(ulong userId, bool isSpeaking) - { - UserId = userId; - IsSpeaking = isSpeaking; - } - } -} diff --git a/src/Discord.Net.Audio/Net/VoiceSocket.cs b/src/Discord.Net.Audio/Net/VoiceSocket.cs deleted file mode 100644 index 9ee5c60e0..000000000 --- a/src/Discord.Net.Audio/Net/VoiceSocket.cs +++ /dev/null @@ -1,516 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.VoiceSocket; -using Discord.Audio; -using Discord.Audio.Opus; -using Discord.Audio.Sodium; -using Discord.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public partial class VoiceSocket : WebSocket - { - private const int MaxOpusSize = 4000; - private const string EncryptedMode = "xsalsa20_poly1305"; - private const string UnencryptedMode = "plain"; - - private readonly int _targetAudioBufferLength; - private readonly ConcurrentDictionary _decoders; - private readonly AudioServiceConfig _audioConfig; - private Task _sendTask, _receiveTask; - private VoiceBuffer _sendBuffer; - private OpusEncoder _encoder; - private uint _ssrc; - private ConcurrentDictionary _ssrcMapping; - private UdpClient _udp; - private IPEndPoint _endpoint; - private bool _isEncrypted; - private byte[] _secretKey, _encodingBuffer; - private ushort _sequence; - private string _encryptionMode; - private int _ping; - private ulong? _userId; - private string _sessionId; - - public string Token { get; internal set; } - public Server Server { get; internal set; } - public VoiceChannel Channel { get; internal set; } - - public int Ping => _ping; - internal VoiceBuffer OutputBuffer => _sendBuffer; - - internal event EventHandler UserIsSpeaking = delegate { }; - internal event EventHandler FrameReceived = delegate { }; - - private void OnUserIsSpeaking(ulong userId, bool isSpeaking) - => UserIsSpeaking(this, new InternalIsSpeakingEventArgs(userId, isSpeaking)); - internal void OnFrameReceived(ulong userId, ulong channelId, byte[] buffer, int offset, int count) - => FrameReceived(this, new InternalFrameEventArgs(userId, channelId, buffer, offset, count)); - - internal VoiceSocket(DiscordConfig config, AudioServiceConfig audioConfig, JsonSerializer serializer, Logger logger) - : base(config, serializer, logger) - { - _audioConfig = audioConfig; - _decoders = new ConcurrentDictionary(); - _targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames - _encodingBuffer = new byte[MaxOpusSize]; - _ssrcMapping = new ConcurrentDictionary(); - _encoder = new OpusEncoder(48000, _audioConfig.Channels, 20, _audioConfig.Bitrate, OpusApplication.MusicOrMixed); - _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); - } - - public Task Connect(string host, string token, ulong userId, string sessionId, CancellationToken parentCancelToken) - { - Host = host; - Token = token; - _userId = userId; - _sessionId = sessionId; - return BeginConnect(parentCancelToken); - } - private async Task Reconnect() - { - try - { - var cancelToken = _parentCancelToken; - await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); - while (!cancelToken.IsCancellationRequested) - { - try - { - await BeginConnect(_parentCancelToken).ConfigureAwait(false); - break; - } - catch (OperationCanceledException) { throw; } - catch (Exception ex) - { - Logger.Error("Reconnect failed", ex); - //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException) { } - } - public async Task Disconnect() - { - await _taskManager.Stop(true).ConfigureAwait(false); - _userId = null; - } - - protected override async Task Run() - { - _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); - - List tasks = new List(); - if (_audioConfig.Mode.HasFlag(AudioMode.Outgoing)) - _sendTask = Task.Run(() => SendVoiceAsync(CancelToken)); - _receiveTask = Task.Run(() => ReceiveVoiceAsync(CancelToken)); - - SendIdentify(_userId.Value, _sessionId); - -#if !DOTNET5_4 - tasks.Add(WatcherAsync()); -#endif - tasks.AddRange(_engine.GetTasks(CancelToken)); - tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); - } - protected override async Task Cleanup() - { - var sendThread = _sendTask; - if (sendThread != null) - { - try { await sendThread.ConfigureAwait(false); } - catch (Exception) { } //Ignore any errors during cleanup - } - _sendTask = null; - - var receiveThread = _receiveTask; - if (receiveThread != null) - { - try { await receiveThread.ConfigureAwait(false); } - catch (Exception) { } //Ignore any errors during cleanup - } - _receiveTask = null; - - OpusDecoder decoder; - foreach (var pair in _decoders) - { - if (_decoders.TryRemove(pair.Key, out decoder)) - decoder.Dispose(); - } - - ClearPCMFrames(); - _udp = null; - - await base.Cleanup().ConfigureAwait(false); - } - - private async Task ReceiveVoiceAsync(CancellationToken cancelToken) - { - var closeTask = cancelToken.Wait(); - try - { - byte[] packet, decodingBuffer = null, nonce = null, result; - int packetLength, resultOffset, resultLength; - IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); - - if ((_audioConfig.Mode & AudioMode.Incoming) != 0) - { - decodingBuffer = new byte[MaxOpusSize]; - nonce = new byte[24]; - } - - while (!cancelToken.IsCancellationRequested) - { - await Task.Delay(1).ConfigureAwait(false); - if (_udp.Available > 0) - { -#if !DOTNET5_4 - packet = _udp.Receive(ref endpoint); -#else - //TODO: Is this really the only way to end a Receive call in DOTNET5_4? - var receiveTask = _udp.ReceiveAsync(); - var task = Task.WhenAny(closeTask, receiveTask).Result; - if (task == closeTask) - break; - var udpPacket = receiveTask.Result; - packet = udpPacket.Buffer; - endpoint = udpPacket.RemoteEndPoint; -#endif - packetLength = packet.Length; - - if (packetLength > 0 && endpoint.Equals(_endpoint)) - { - if (State != ConnectionState.Connected) - { - if (packetLength != 70) - return; - - string ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - int port = packet[68] | packet[69] << 8; - - SendSelectProtocol(ip, port); - if ((_audioConfig.Mode & AudioMode.Incoming) == 0) - return; //We dont need this thread anymore - } - else - { - //Parse RTP Data - if (packetLength < 12) return; - if (packet[0] != 0x80) return; //Flags - if (packet[1] != 0x78) return; //Payload Type - - ushort sequenceNumber = (ushort)((packet[2] << 8) | - packet[3] << 0); - uint timestamp = (uint)((packet[4] << 24) | - (packet[5] << 16) | - (packet[6] << 8) | - (packet[7] << 0)); - uint ssrc = (uint)((packet[8] << 24) | - (packet[9] << 16) | - (packet[10] << 8) | - (packet[11] << 0)); - - //Decrypt - if (_isEncrypted) - { - if (packetLength < 28) //12 + 16 (RTP + Poly1305 MAC) - return; - - Buffer.BlockCopy(packet, 0, nonce, 0, 12); - int ret = SecretBox.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey); - if (ret != 0) - continue; - result = decodingBuffer; - resultOffset = 0; - resultLength = packetLength - 28; - } - else //Plain - { - result = packet; - resultOffset = 12; - resultLength = packetLength - 12; - } - - /*if (_logLevel >= LogMessageSeverity.Debug) - RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/ - - ulong userId; - if (_ssrcMapping.TryGetValue(ssrc, out userId)) - OnFrameReceived(userId, Channel.Id, result, resultOffset, resultLength); - } - } - } - } - } - catch (OperationCanceledException) { } - catch (InvalidOperationException) { } //Includes ObjectDisposedException - } - - private async Task SendVoiceAsync(CancellationToken cancelToken) - { - try - { - while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected) - await Task.Delay(1).ConfigureAwait(false); - - if (cancelToken.IsCancellationRequested) - return; - - byte[] frame = new byte[_encoder.FrameSize]; - byte[] encodedFrame = new byte[MaxOpusSize]; - byte[] voicePacket, pingPacket, nonce = null; - uint timestamp = 0; - double nextTicks = 0.0, nextPingTicks = 0.0; - long ticksPerSeconds = Stopwatch.Frequency; - double ticksPerMillisecond = Stopwatch.Frequency / 1000.0; - double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; - double spinLockThreshold = 3 * ticksPerMillisecond; - uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; - Stopwatch sw = Stopwatch.StartNew(); - - if (_isEncrypted) - { - nonce = new byte[24]; - voicePacket = new byte[MaxOpusSize + 12 + 16]; - } - else - voicePacket = new byte[MaxOpusSize + 12]; - - pingPacket = new byte[8]; - - int rtpPacketLength = 0; - voicePacket[0] = 0x80; //Flags; - voicePacket[1] = 0x78; //Payload Type - voicePacket[8] = (byte)(_ssrc >> 24); - voicePacket[9] = (byte)(_ssrc >> 16); - voicePacket[10] = (byte)(_ssrc >> 8); - voicePacket[11] = (byte)(_ssrc >> 0); - - if (_isEncrypted) - Buffer.BlockCopy(voicePacket, 0, nonce, 0, 12); - - bool hasFrame = false; - while (!cancelToken.IsCancellationRequested) - { - if (!hasFrame && _sendBuffer.Pop(frame)) - { - ushort sequence = unchecked(_sequence++); - voicePacket[2] = (byte)(sequence >> 8); - voicePacket[3] = (byte)(sequence >> 0); - voicePacket[4] = (byte)(timestamp >> 24); - voicePacket[5] = (byte)(timestamp >> 16); - voicePacket[6] = (byte)(timestamp >> 8); - voicePacket[7] = (byte)(timestamp >> 0); - - //Encode - int encodedLength = _encoder.EncodeFrame(frame, 0, encodedFrame); - - //Encrypt - if (_isEncrypted) - { - Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce - int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey); - if (ret != 0) - continue; - rtpPacketLength = encodedLength + 12 + 16; - } - else - { - Buffer.BlockCopy(encodedFrame, 0, voicePacket, 12, encodedLength); - rtpPacketLength = encodedLength + 12; - } - - timestamp = unchecked(timestamp + samplesPerFrame); - hasFrame = true; - } - - long currentTicks = sw.ElapsedTicks; - double ticksToNextFrame = nextTicks - currentTicks; - if (ticksToNextFrame <= 0.0) - { - if (hasFrame) - { - try - { - _udp.Send(voicePacket, rtpPacketLength); - } - catch (SocketException ex) - { - Logger.Error("Failed to send UDP packet.", ex); - } - hasFrame = false; - } - nextTicks += ticksPerFrame; - - //Is it time to send out another ping? - if (currentTicks > nextPingTicks) - { - //Increment in LE - for (int i = 0; i < 8; i++) - { - var b = pingPacket[i]; - if (b == byte.MaxValue) - pingPacket[i] = 0; - else - { - pingPacket[i] = (byte)(b + 1); - break; - } - } - await _udp.SendAsync(pingPacket, pingPacket.Length).ConfigureAwait(false); - nextPingTicks = currentTicks + 5 * ticksPerSeconds; - } - } - else - { - if (hasFrame) - { - int time = (int)Math.Floor(ticksToNextFrame / ticksPerMillisecond); - if (time > 0) - await Task.Delay(time).ConfigureAwait(false); - } - else - await Task.Delay(1).ConfigureAwait(false); //Give as much time to the encrypter as possible - } - } - } - catch (OperationCanceledException) { } - catch (InvalidOperationException) { } //Includes ObjectDisposedException - } -#if !DOTNET5_4 - //Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken - private async Task WatcherAsync() - { - await CancelToken.Wait().ConfigureAwait(false); - _udp.Close(); - } -#endif - - protected override async Task ProcessMessage(string json) - { - await base.ProcessMessage(json).ConfigureAwait(false); - - WebSocketMessage msg; - using (var reader = new JsonTextReader(new StringReader(json))) - msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage; - - var opCode = (OpCodes)msg.Operation; - switch (opCode) - { - case OpCodes.Ready: - { - if (State != ConnectionState.Connected) - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - _heartbeatInterval = payload.HeartbeatInterval; - _ssrc = payload.SSRC; - var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); - _endpoint = new IPEndPoint(address, payload.Port); - - if (_audioConfig.EnableEncryption) - { - if (payload.Modes.Contains(EncryptedMode)) - { - _encryptionMode = EncryptedMode; - _isEncrypted = true; - } - else - throw new InvalidOperationException("Unexpected encryption format."); - } - else - { - _encryptionMode = UnencryptedMode; - _isEncrypted = false; - } - _udp.Connect(_endpoint); - - _sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue); - //No thread issue here because SendAsync doesn't start until _isReady is true - byte[] packet = new byte[70]; - packet[0] = (byte)(_ssrc >> 24); - packet[1] = (byte)(_ssrc >> 16); - packet[2] = (byte)(_ssrc >> 8); - packet[3] = (byte)(_ssrc >> 0); - await _udp.SendAsync(packet, 70).ConfigureAwait(false); - } - } - break; - case OpCodes.Heartbeat: - { - long time = EpochTime.GetMilliseconds(); - var payload = (long)msg.Payload; - _ping = (int)(payload - time); - //TODO: Use this to estimate latency - } - break; - case OpCodes.SessionDescription: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - _secretKey = payload.SecretKey; - SendSetSpeaking(true); - await EndConnect().ConfigureAwait(false); - } - break; - case OpCodes.Speaking: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - OnUserIsSpeaking(payload.UserId, payload.IsSpeaking); - } - break; - default: - Logger.Warning($"Unknown Opcode: {opCode}"); - break; - } - } - - public void SendPCMFrames(byte[] data, int offset, int count) - { - _sendBuffer.Push(data, offset, count, CancelToken); - } - public void ClearPCMFrames() - { - _sendBuffer.Clear(CancelToken); - } - - public void WaitForQueue() - { - _sendBuffer.Wait(CancelToken); - } - - public override void SendHeartbeat() - => QueueMessage(new HeartbeatCommand()); - public void SendIdentify(ulong id, string sessionId) - => QueueMessage(new IdentifyCommand - { - GuildId = Server.Id, - UserId = id, - SessionId = sessionId, - Token = Token - }); - public void SendSelectProtocol(string externalAddress, int externalPort) - => QueueMessage(new SelectProtocolCommand - { - Protocol = "udp", - ExternalAddress = externalAddress, - ExternalPort = externalPort, - EncryptionMode = _encryptionMode - }); - public void SendSetSpeaking(bool value) - => QueueMessage(new SetSpeakingCommand { IsSpeaking = value, Delay = 0 }); - - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusConverter.cs b/src/Discord.Net.Audio/Opus/OpusConverter.cs deleted file mode 100644 index d93337138..000000000 --- a/src/Discord.Net.Audio/Opus/OpusConverter.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Runtime.InteropServices; -#if NET45 -using System.Security; -#endif - -namespace Discord.Audio.Opus -{ - internal enum OpusApplication : int - { - Voice = 2048, - MusicOrMixed = 2049, - LowLatency = 2051 - } - internal enum OpusError : int - { - OK = 0, - BadArg = -1, - BufferToSmall = -2, - InternalError = -3, - InvalidPacket = -4, - Unimplemented = -5, - InvalidState = -6, - AllocFail = -7 - } - - internal abstract class OpusConverter : IDisposable - { - protected enum Ctl : int - { - SetBitrateRequest = 4002, - GetBitrateRequest = 4003, - SetInbandFECRequest = 4012, - GetInbandFECRequest = 4013 - } - -#if NET45 - [SuppressUnmanagedCodeSecurity] -#endif - protected unsafe static class UnsafeNativeMethods - { - [DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error); - [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] - public static extern void DestroyEncoder(IntPtr encoder); - [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] - public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); - [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - public static extern int EncoderCtl(IntPtr st, Ctl request, int value); - - [DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error); - [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] - public static extern void DestroyDecoder(IntPtr decoder); - [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] - public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); - } - - protected IntPtr _ptr; - - /// Gets the bit rate of this converter. - public const int BitsPerSample = 16; - /// Gets the input sampling rate of this converter. - public int InputSamplingRate { get; } - /// Gets the number of channels of this converter. - public int InputChannels { get; } - /// Gets the milliseconds per frame. - public int FrameLength { get; } - /// Gets the number of samples per frame. - public int SamplesPerFrame { get; } - /// Gets the bytes per frame. - public int FrameSize { get; } - /// Gets the bytes per sample. - public int SampleSize { get; } - - protected OpusConverter(int samplingRate, int channels, int frameLength) - { - if (samplingRate != 8000 && samplingRate != 12000 && - samplingRate != 16000 && samplingRate != 24000 && - samplingRate != 48000) - throw new ArgumentOutOfRangeException(nameof(samplingRate)); - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - - InputSamplingRate = samplingRate; - InputChannels = channels; - FrameLength = frameLength; - SampleSize = (BitsPerSample / 8) * channels; - SamplesPerFrame = samplingRate / 1000 * FrameLength; - FrameSize = SamplesPerFrame * SampleSize; - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - disposedValue = true; - } - ~OpusConverter() { - Dispose(false); - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - } -} diff --git a/src/Discord.Net.Audio/Opus/OpusDecoder.cs b/src/Discord.Net.Audio/Opus/OpusDecoder.cs deleted file mode 100644 index d8e6b8087..000000000 --- a/src/Discord.Net.Audio/Opus/OpusDecoder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace Discord.Audio.Opus -{ - internal class OpusDecoder : OpusConverter - { - /// Creates a new Opus decoder. - /// Sampling rate of the input PCM (in Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000 - /// Length, in milliseconds, of each frame. Supported Values: 2.5, 5, 10, 20, 40, or 60 - public OpusDecoder(int samplingRate, int channels, int frameLength) - : base(samplingRate, channels, frameLength) - { - OpusError error; - _ptr = UnsafeNativeMethods.CreateDecoder(samplingRate, channels, out error); - if (error != OpusError.OK) - throw new InvalidOperationException($"Error occured while creating decoder: {error}"); - } - - /// Produces PCM samples from Opus-encoded audio. - /// PCM samples to decode. - /// Offset of the frame in input. - /// Buffer to store the decoded frame. - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output) - { - int result = 0; - fixed (byte* inPtr = input) - result = UnsafeNativeMethods.Decode(_ptr, inPtr + inputOffset, inputCount, output, SamplesPerFrame, 0); - - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - return result; - } - - protected override void Dispose(bool disposing) - { - if (_ptr != IntPtr.Zero) - { - UnsafeNativeMethods.DestroyDecoder(_ptr); - _ptr = IntPtr.Zero; - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs deleted file mode 100644 index be0623c6b..000000000 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -namespace Discord.Audio.Opus -{ - internal class OpusEncoder : OpusConverter - { - /// Gets the bit rate in kbit/s. - public int? BitRate { get; } - /// Gets the coding mode of the encoder. - public OpusApplication Application { get; } - - /// Creates a new Opus encoder. - /// Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000 - /// Number of channels in input signal. Supported Values: 1 or 2 - /// Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60 - /// Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. - /// Coding mode. - public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, OpusApplication application) - : base(samplingRate, channels, frameLength) - { - if (bitrate != null && (bitrate < 1 || bitrate > AudioServiceConfig.MaxBitrate)) - throw new ArgumentOutOfRangeException(nameof(bitrate)); - - BitRate = bitrate; - Application = application; - - OpusError error; - _ptr = UnsafeNativeMethods.CreateEncoder(samplingRate, channels, (int)application, out error); - if (error != OpusError.OK) - throw new InvalidOperationException($"Error occured while creating encoder: {error}"); - - SetForwardErrorCorrection(true); - if (bitrate != null) - SetBitrate(bitrate.Value); - } - - /// Produces Opus encoded audio from PCM samples. - /// PCM samples to encode. - /// Offset of the frame in pcmSamples. - /// Buffer to store the encoded frame. - /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) - { - int result = 0; - fixed (byte* inPtr = input) - result = UnsafeNativeMethods.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); - - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - return result; - } - - /// Gets or sets whether Forward Error Correction is enabled. - public void SetForwardErrorCorrection(bool value) - { - var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0); - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - } - - /// Gets or sets whether Forward Error Correction is enabled. - public void SetBitrate(int value) - { - var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000); - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - } - - protected override void Dispose(bool disposing) - { - if (_ptr != IntPtr.Zero) - { - UnsafeNativeMethods.DestroyEncoder(_ptr); - _ptr = IntPtr.Zero; - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Sodium/SecretBox.cs b/src/Discord.Net.Audio/Sodium/SecretBox.cs deleted file mode 100644 index f73093316..000000000 --- a/src/Discord.Net.Audio/Sodium/SecretBox.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Runtime.InteropServices; -#if NET45 -using System.Security; -#endif - -namespace Discord.Audio.Sodium -{ - internal unsafe static class SecretBox - { -#if NET45 - [SuppressUnmanagedCodeSecurity] -#endif - private static class SafeNativeMethods - { - [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] - public static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); - [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] - public static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); - } - - public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) - { - fixed (byte* outPtr = output) - return SafeNativeMethods.SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); - } - public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) - { - fixed (byte* inPtr = input) - return SafeNativeMethods.SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); - } - } -} diff --git a/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs b/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs deleted file mode 100644 index 698f44d4c..000000000 --- a/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Discord -{ - public class UserIsSpeakingEventArgs : UserEventArgs - { - public bool IsSpeaking { get; } - - public UserIsSpeakingEventArgs(User user, bool isSpeaking) - : base(user) - { - IsSpeaking = isSpeaking; - } - } -} diff --git a/src/Discord.Net.Audio/VirtualClient.cs b/src/Discord.Net.Audio/VirtualClient.cs deleted file mode 100644 index 9c8100e47..000000000 --- a/src/Discord.Net.Audio/VirtualClient.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - internal class VirtualClient : IAudioClient - { - private readonly AudioClient _client; - - public Server Server { get; } - - public int Id => 0; - public string SessionId => _client.Server == Server ? _client.SessionId : null; - - public ConnectionState State => _client.Server == Server ? _client.State : ConnectionState.Disconnected; - public VoiceChannel Channel => _client.Server == Server ? _client.Channel : null; - public CancellationToken CancelToken => _client.Server == Server ? _client.CancelToken : CancellationToken.None; - - public RestClient ClientAPI => _client.Server == Server ? _client.ClientAPI : null; - public GatewaySocket GatewaySocket => _client.Server == Server ? _client.GatewaySocket : null; - public VoiceSocket VoiceSocket => _client.Server == Server ? _client.VoiceSocket : null; - - public VirtualClient(AudioClient client, Server server) - { - _client = client; - Server = server; - } - - public Task Disconnect() => _client.Service.Leave(Server); - public Task Join(VoiceChannel channel) => _client.Join(channel); - - public void Send(byte[] data, int offset, int count) => _client.Send(data, offset, count); - public void Clear() => _client.Clear(); - public void Wait() => _client.Wait(); - } -} diff --git a/src/Discord.Net.Audio/VoiceBuffer.cs b/src/Discord.Net.Audio/VoiceBuffer.cs deleted file mode 100644 index 054ab81a0..000000000 --- a/src/Discord.Net.Audio/VoiceBuffer.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Threading; - -namespace Discord.Audio -{ - internal class VoiceBuffer - { - private readonly int _frameSize, _frameCount, _bufferSize; - private readonly byte[] _buffer; - private readonly byte[] _blankFrame; - private ushort _readCursor, _writeCursor; - private ManualResetEventSlim _notOverflowEvent; - private bool _isClearing; - private AsyncLock _lock; - - public int FrameSize => _frameSize; - public int FrameCount => _frameCount; - public ushort ReadPos => _readCursor; - public ushort WritePos => _writeCursor; - - public VoiceBuffer(int frameCount, int frameSize) - { - _frameSize = frameSize; - _frameCount = frameCount; - _bufferSize = _frameSize * _frameCount; - _readCursor = 0; - _writeCursor = 0; - _buffer = new byte[_bufferSize]; - _blankFrame = new byte[_frameSize]; - _notOverflowEvent = new ManualResetEventSlim(); //Notifies when an overflow is solved - _lock = new AsyncLock(); - } - - public void Push(byte[] buffer, int offset, int count, CancellationToken cancelToken) - { - if (cancelToken.IsCancellationRequested) - throw new OperationCanceledException("Client is disconnected.", cancelToken); - - int wholeFrames = count / _frameSize; - int expectedBytes = wholeFrames * _frameSize; - int lastFrameSize = count - expectedBytes; - - using (_lock.Lock()) - { - for (int i = 0, pos = offset; i <= wholeFrames; i++, pos += _frameSize) - { - //If the read cursor is in the next position, wait for it to move. - ushort nextPosition = _writeCursor; - AdvanceCursorPos(ref nextPosition); - if (_readCursor == nextPosition) - { - _notOverflowEvent.Reset(); - try - { - _notOverflowEvent.Wait(cancelToken); - } - catch (OperationCanceledException ex) - { - throw new OperationCanceledException("Client is disconnected.", ex, cancelToken); - } - } - - if (i == wholeFrames) - { - //If there are no partial frames, skip this step - if (lastFrameSize == 0) - break; - - //Copy partial frame - Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, lastFrameSize); - - //Wipe the end of the buffer - Buffer.BlockCopy(_blankFrame, 0, _buffer, _writeCursor * _frameSize + lastFrameSize, _frameSize - lastFrameSize); - } - else - { - //Copy full frame - Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, _frameSize); - } - - //Advance the write cursor to the next position - AdvanceCursorPos(ref _writeCursor); - } - } - } - - public bool Pop(byte[] buffer) - { - //using (_lock.Lock()) - //{ - if (_writeCursor == _readCursor) - { - _notOverflowEvent.Set(); - return false; - } - - bool isClearing = _isClearing; - if (!isClearing) - Buffer.BlockCopy(_buffer, _readCursor * _frameSize, buffer, 0, _frameSize); - - //Advance the read cursor to the next position - AdvanceCursorPos(ref _readCursor); - _notOverflowEvent.Set(); - return !isClearing; - //} - } - - public void Clear(CancellationToken cancelToken) - { - using (_lock.Lock()) - { - _isClearing = true; - for (int i = 0; i < _frameCount; i++) - Buffer.BlockCopy(_blankFrame, 0, _buffer, i * _frameCount, i++); - - _writeCursor = 0; - _readCursor = 0; - _isClearing = false; - } - } - - public void Wait(CancellationToken cancelToken) - { - while (true) - { - _notOverflowEvent.Wait(cancelToken); - if (_writeCursor == _readCursor) - break; - } - } - - private void AdvanceCursorPos(ref ushort pos) - { - pos++; - if (pos == _frameCount) - pos = 0; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs b/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs deleted file mode 100644 index 4f46abde2..000000000 --- a/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord -{ - public class VoiceDisconnectedEventArgs : DisconnectedEventArgs - { - public ulong ServerId { get; } - - public VoiceDisconnectedEventArgs(ulong serverId, bool wasUnexpected, Exception ex) - : base(wasUnexpected, ex) - { - ServerId = serverId; - } - } -} diff --git a/src/Discord.Net.Audio/libsodium.dll b/src/Discord.Net.Audio/libsodium.dll deleted file mode 100644 index a9ab5078e7b8537c60a0f032e6e336b51a8f67e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 492032 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 diff --git a/src/Discord.Net.Audio/opus.dll b/src/Discord.Net.Audio/opus.dll deleted file mode 100644 index a9eec802c68d7cff490baecb31a9156bddfca615..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 271872 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�m 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
P7Ry|dL3OFER0TZc7S`(
zo#J|f`gOSZngz+4IA;?Vo^W+rza|!|UzcoJgYOA^Hv-yCY$Y7%R)+Okw5fyy6^`!O
zRW*ZEf`|cKR}>Ld=#E?cR+n>^*3YHN#ch4vJm1xSDwwgQf3SMDmD7lt0OTgF$bgFO
za^z-PG0E-(Cb_IFEZp$WkWl^yYso*tmO@TR$ZFYIbc<}0f=kw~!P=_utAC6}B1)jX2TduTtMp%(>)x-k_e5-s
zocV^_;bCZ(>~lNv7B!cCv+=f}z#gbPjWLG2@=!SKEE4z`DW~%9&hb>P77HFiEJaU+4F?GZ|O*|MDI)6UWWVj((=u>%y<{
z0`957d-h^&BKC7%Sf#6rdE98jth(6W=k36toD4le;68k9$Z*|>D`W@An{_%T?2rg-
z#yW15pS*JUW&MNxo@;3GlWLb6@rQtp-6slWtVDMjBs77s9^XvwWpWGE7euOi{qQ!5
zG>!9{HgQwM&J%C9Z{iiB3XfvADhjlAB
zxi#=kw{7l`l!XnltIO6%(;D|MQwV}HI{PA8?l-M;51+Az7Wbe%5g>S7r+r*ETBWh4
z{H7H4778x`=Gw(=35jGF{~~vEn7^>$J^8`hL9AmW<9m@eWPnWShfT%n)D?R=#-)?>J-^QPt5#tfGI2)AOOF?Hji-ai
zntp(5u{WAPz~lDojgZ3RsS%C1y`y8Vrk41VN`k4S{-n}iYMDQ&
zO!spql?PKR{7DtT)Y1N=(ZSTQ{-m+N)N%f#alzCoe^OO2b%H->LNK+)pHyRwh;I{q
zORnIx{;TjC(<*^b{laxVI0K)h`=Kkr>t4BD@2cO2w{(6Uhivl$OD+hK>hldEqz?GG
zrEm~S4fpkFhvIo)KD1hiWRjMY_&^6(vk*`&6`Q&C_lt^XAz4hzu^lPZ>*tdSw
z!W{ZK+P+$gvb_pAH146V6>lvod>Z;%{?@NLoD6*}d+S$;1%|$sUig*w$qvmSs)qb9
zUhQX=O$@oEbl6|8F2_%N6>n1c$!r|UhqL}`EGp~u>ylgAx;injeiQy`7OZ1K$pqni
zHHSj)+qHI6)iTSd&i{6;x4CJJ^~5B~F(&W=nX+*sCRoNL31+Nj(7v7v1zeb!J?#kV
zU}Alnmmb412|^*ElWv6Z7Ey3GZmohKv_-+4xf|jEH{Nb23}zMiHxTNl*uSATm{sE6
zP!h~4^^xz+D)VnB3ucx3He8%77S#`-sm4Q7q=Zx|QMs`78B3T934
zZ>N^d>vzA@{ugP!sg>Wzq{1mHi0f=
zci>@$tfr`P+-XL^1X;i77g*ZzwE2jZ`2SB&n-NP`zbShe_YA~r-S3#KY^#)LGuG{-
zQg=TBHv*@*A<@6=1GpTh=>KIq-TGuRP_k`2TLpNk`%qT)ANwU?{AL_F_I!08y8wxP
zIt~?|QD`jT`Lo`GdH6BQiYX2w
z`@`$bIfi-Kv)H&E?st*`_<-wQyvM&&6`VLNbF@|~dfuVLo=O}_+`>y7O8j!UVq5G#
z?^0rMznC#|E3yA1Ocmxahm3%7JyxAp9?KsPrpDchEYO
zBbFVovLA$#fX?|(>0%?~K)eCMMm{$q0gb`2zsFnkuu9;RgaCdFcN$(YYQvIm`9DAQ
zQE$i9RKt6(>+JAqPN*6jjo*0IRkJ)^eXQ|-<|g}1Dzo$DTS0GNHsBjv#M0j0fKb3D
z4M+@4T6DfP@K^R|0HsO86Qz-z2V
z%|cMX(fGsO_U~O#f*jlxYu&&X$J?HM
zOS$viylU96p0K=c1;K@FycQc87Ht&!H89;Z=pIqvHxy)<5%ksZ%
zT82Q~s{GNkIDejR?bG@Gf5Fvl({jyU(EpEJ(EoD(#tN_Xga1YU&!WR{{U7ard92qO
zd*}W~i!na%r1D4oe+DvtexVGZ)evm{eW>nn{+Fw~RdO|znLL5N{74Buv~!I=H~y9J
z2hs;F2siDmQpCpeD;XDdIx`twkOo&N($4r-^d2rjRY|(f`#HQ%pUX@xb#Keu+pj2h
zl)v14sc>I@73Cl8-XbklE$1x1>KW(W!;bO3buye)wB8`w61yQJ%NxS)fo&8-t}%In
z|LvL_7iE0doQIZ>Q7-kkh;~9A;PY^ol)jJOoUZW4dbfXkccd-+5f0#U{@3v(j|2FT
zMC_aYG39!7r+zY1ZWS9f{#*yWTArD*P>{$>`H~>?<>waq1NTUo(i1;%+W#zv;%Uiq
z@rIGXXXhV#iTEH>Zu3;OJ2PdpT*qgojCDWb+)tH$9PzRJ9zT0O{fg@;L6W@GjWkId
zzl`Z_U2i-%Be>!uZV)kzzPzb^{J77JV$)Q_f3iI?yi+
zTviL;Az|$UvJ3F>?Cc7ts06%|+{W|UPa^4ye;#%XZ(degls%j>&UTtd$$+C#q=8}l
zdg7BV+J7pI;-z@m<=7u4voUW+{Q5=sqpz&H9^*MAFV{C_8Li)RKXU)Miz-L*fcO5n
zPN!+%=eYDUfzv&{i!%>OOVN(1nA>yfD^Dg*7NCRR5Y>OW=X8Ghxd5nbRhCTs1N_bN
zr+2jbGqx76DYGKHae%oH`y$Hf@bHe7Io7
z79B{Id((Pj3m!=R^VS$jMpxbPpD>4HYJAS$XLq%BNXR8Uy4q-eL
z-egyjcw>rzJ`)STO!vHWNzcFtOj+=z(mXaeUJcD)gL&}{F1rNJK4ND511`}Q)iw2
zQ!>n8gilflUO#ZaML`YpRSckCy6C(uICew31-ssLLCJ=Vat%FXCf+A`wuITSIa_sD
zKOOhd?*|0wbK%qi?^7APq39?k0h%|=x1Sxws(ATMN3s1U?HGn{zsJ;1O_%I7>=xo<
z^q9u_!gwvwb9&K!O03hJU=$lud*`ZAuXkh@rHjJxi}UTV7tH#=kI
za2G8;rta@XN8-Estu{F`3~PG}^g;hKv~SeXkyLHo%aV1OrZ%pTClxp#tUpw}xA8~F
zUhvF%%jXwxzoRf(S>RLlN9>AVr8W{zMFYE6^$N5|R`tsW_Q2SE#>7@&szf~$sST-_qt+5Zm9ew1$H{2zGe45tQ;4p6f
z#P2IkuGgAWb=Q`$G*Z7)GMB?kckA(6oSK|B3Myj+=Z;CqL)+-tzf9!S~fk%U10gn`u9oie`y`=JFo)dqIvpZB2h~Eib8o5Tzi)
z++8m8*7kVY9}SB_F$9@w9Y=Jnm;|1TU7}Aypku9VZV3Jz^e}pv{RU~ahld2g;s_Lk
z-l0D6JXt(`leJXnmbswP>>%R|=9LivYpXi7Q-Uy#J|)4DsxhB0CAO~G4CC9`0|>S>
zwJKXghGBE3q&x52hNNkJL8@ZCy39eHAN@`v|8joo^vqseOEY!BO3N0Mq#!5IGMf5Sou(@tEK-oL@12{F}i8CdnY%*kkX<
z_>|vK$wwAK8=doONPS-W9H3e`=(CouLn+F!rUnT{v0o;pV{~%hYD!m7>l*TsET^Ml
zE+pur-C&V`XL(PC0^Hv
zQX)%K?_FBVSnFbhz^E(m?D_yBXmOdLzVdmi2?5{FE5s
zu%;`P@j7cJ_WWk#BEKdl+5MGNR6V1JfpC}-HUYTf^^21a-EgNkpkJx_l1Qa%`im52qovtk+_h$>peNoRZM^+w2(TCN&E(SWE
zn7RTM))X*~8vKeOJ_zQaYQ#Rp>->h*f82ioW=YN~T|cY`A$KZ~b{doXcVxYqry7Kz
zbO`BaCA!a#ne!&3q^7St%V_D;GR`(qW6Gu(FBalN&Vs;l?_UIEFnD>J|I^y~gEbU<
ztvo98s+BouWf<4Iit~=T0neavpVs?Qd+%*7tFQt!74{{lsR+xyo^!+Lyh>RE1&UN$
zRuRmripm|1+M&J|gLxCq7ttiB2yKkRbp0-&Y6kCS$`6;R7-Tlp>SFt}%yse_J>4S!x
zav6_6lBLlCQ)IHz62Fj@9M@x9OM1ja@Y+j{WB*J>k=JueB)2|@KcCq7b0~g(Ls2e|
zj&+MIezo>cIEQ;q>^wvkpS8F36A*&ncKDS{yaG@DI10`iggxs$!S%dHW!vf0ls%5^
z6B3TiA6P~yA~O7*fknlLQq;R>L|)-Krg-4B7oK{DJ60pU#Yh;!#h<
zxbf$;yvRCBp1r#LYOS}{gZkUG+8DRZyK_f_#ogcMki7VS{)#0*2Z|9btwJI52LAAI
z>-a*_{@#J#@YM6`LKyezy}{QDm;FsH+mrgBx67vGUhQe$Ax>KpA(K%0_hpYUev&6efJTF!q{jdm?8
z)4OW(jX2@Er`80$k-_c4Pe$L#vU`}9=recgL`_K?7_0IvLfMXVRkD>XFX0`cr!Lq{ibD{aw#>y8B-P^r!kMNlgtKzP2tbM6UIX}ACX^L+laX72mk=jA%*T<1F1xz6>%A0M!OgFs=MmN9&u8=z%;w$V}#zS?<8~r
zt-I5o8Q!tu>5i@6S&h}(3p@G<7h&&%62>}HXRL_X?@%eZ-0508yAwA-cOFK;qi*)S
zoMx-ji+{)TCVs0?)6{w$#k6JzL5EyX_{^uw{N#`_s#uTWeGHmT;*p@&`WWP~Shdv(
zZ6Z=g0|Cct414wWl0Xyc5KqB`nM;wKC2GN#t@|eb!Jnui+*Zj`s=-%
zgJ`a?P%uA^L^A=%MdDGR!^1j^Cw?n_HD+)L+eu+dQ8{No4l%~>LLm59!hF1-zQpNR
zXAUSO@DE`sGz@`f9WxmCRF&Yn`zuszEeL;WRz_qw_>7#Xz~C`KP`*NVR-nB+ACS~H
z_{C7B?cbz~SEOEZ3Nl@`dh<(uBIMN)A*2`Wj_bkOnQ$I#szfdstSs*_%A`5|SvR-b
z#5gNgQG~6z)C=BXY84w=6D1{z+TuXQTX+kXx_O3#`1gn8fQ|bbDu^QNfSGk3{7@{J
zj}{eXj>nt_zjZ=v2FN#eOw53MmOcYNAWx2i0Zz=pzzfxx)@_sz1r!QB7>xz#ADFlJ)VN&ze4bX|&qXp(TX2c`u!PwyTWE4V3wiR!m~d_^VL
zdGKQY#1QeLPeubA(jf;p32rkx_!O!2(d;;DVBh
zm515$;?bW{M_>fI8AX5T>a+YP8};|3iddO*f0YBQWOo@%=b$(FSEY7N4+9xV8
zf4#?KOE3Mor|=&y2UW%~)BUKNI`_Be51h7hKidctPb^S6a2T%=eV|B5jDRz4+UcrW*phu+D-r4P^miWm(MRQm
z=wYPn#=)+qnBq9<#26br=ET=PzHWHtIpB=-T$KF^xj1ZL
zW4Y?Hbbxw`yme7wma%RQ9Mqh|@O$y#x%g!2go$PHJDUo+ow&A#f%(We5OK$j1Y17G
z8mc**Hj`)(2PKDC5`RI31)PTiJveZBkTQ_Ol1ZHw1
z=(@%3ym%dITXl=UuDTZ5ij4$Ye?|S%2OnaVmQH_%TcqwpQ+$5)Hf)3jP#vST>YY;V
zv7Qmgzqfij-l%CUQMa+KuWq9h-4Iu-Ya%Hxc93mvU7a^epb1E{&YbmibuEb%H9zIm
zU9--BzQ(+<@3yogUkSP^s`L@NUN3H*PMY_AIqc2}n;^b)4jl?QSQoF$*3u}0Uqx`1
zy2KQ3akPoBg~=oNdE7x1RgZEx@SZvUIbs^kmYu1l%EApIL0QEXH3Ep~gWNi-SR>%7
zlk|OyyFPM9#LCS#QSQ#_Z2U6`ZL42c
zdI(CTtlW0wO>5t@kH?8bR_BrS*X|>ggZ?J{+!gKSM>LR>E%vd3$0zLL45uq5*58XP
zThRTFuTn8os=x5kFhl-`Y}T~7RXXwCl06ZVTP`@?>s-sA(r<}hf|=jJcgG&7(i{!
zn*XEmoGfiY12{-pl^bvOV63}T{c8B+_=qwo{s@svjFblCU4!u9+jDjKI50j40jfDU
z_I^w>d)_klnPJ7HRY;gh??$vwKSz8LmXuCJ|A~s5al-Z?xBiKNe|Q21=Q57z=N@-S
z<%z{jGU{x7*6!c4Vg6cX;nA$|qGMj<4E(0Qg%TT#$Sc_74G=rS3x6i?7W!e#D1kQ>
z#Oat(Ky~VOFsc%zRo6e;x*k4Jo(fPUg`b{v%)CCK6I}3%M76+N#rwvwL=YWnj8H1^
z&zuN0=<*aj&yiSnSWqA8#H{k4i->f35HeA$q#=?LS5@X>k#3RYBN`g)Z3=`ro9HQ9
z+-sjD7TiU2Aaaq;AoDI7^CDjx7-e-2i~`^+XFW;fz*=H3puQ3WpzyWNGG8?DJ(|eX
z|EycT!JBD@aK34|-~W;{21-x3OP(OULstouefTtg|LQ_QBKFK^m3G~fAAt8fghVCL
z`s@IX${56Ecitsq?o;JM7t~k6n#~%68(qSjR7fB7Duvn#c1ekkE0I7qd7L%9v7^ML
zf|32$EJ$Br1Aol@7?*YC(53q^g6%1C!6tBu7>{=O*F#f95IFg6;0NQOl<={54&~){
zmJmb}az<>9+ioyJUl|WxDDsjKbf7?l??W
zVLz59;3&=05dJ#yrV=!Qm_Jg@tLfUX*$E%|M)7+zX!lX&37sHedCQehY
z1S*@kyCBMs!qe<6;DpsLzf*F6f;%u{Z>oE|z>t&+W7!Wno!LSY=f`*$y?k?GNl7(Y
z_LH5J!=hYc+*_JSde*zm^c~OoIA+TZOMUd{V0k}Y(x>t1T@j7CiD=YqdUxlJSKPNu|6DHMS({vA
zq2i9tr}3TD>mEEjY`v*swc@Oo+fnm%8#ZC*(RIjOGO(Q$oGo%XH`7G|6&;zj^t>HM
z+mGhe%5sW7OG}#3Zy9v{ddKhw5=)*nY30TWro@FG&`REfOtys9v1^j9s*yduAU7NM
z`^t?bS_N}CLKkm_GSs6u=XAURrrMQH@pGHq`kys-bQ2)Mi#$%3;kSR!UmS;U{4-{2
z&Wbufgg$V5E0JLF1ys8YiudPkf!jwv4Wc-!uBK|1%iigTbNiE{XP~LU>porx%4%_!
zQ9c9oMOP#9`!5NQ5<1sczgJBYP|Ez&g0Rf*48ZQIzemrT*<#n%6Ury#LrDjKzO=l*
zxj+kcR%v2M#ab`Y#ZA69o8q}1qx>F(9Id)YoxbW>FbFMap7V_1Tx@@Uk5}}#le48H
zdciI%`ut9ROtt*+PHW*WZ8ja|8sSgraz0&tcW0a3B|q~!Ioy`xXKlEx@0xI1OEzS`
z!76TUKl)^ib^E|rJ|+zD2UNu1=TMJkxAkWx-BuSE{uCKyQoEP#=EinHnQv1DITQ2m
zx6ha@n*l5HFFLQFYL1#pL#av3bK<|UiGB6rrrF7hjeb8wA$0iKC!F<*I3D*wbId!K
z(tzWj@~4t}ry#kNVQ2*i!;&{7dM_#

}#i{~|nB-6zSwGaRHWJ~`gk0`9dN64&k< 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 _runFunc; - internal readonly Dictionary _parametersByName; - - public string Text { get; } - public string Category { get; internal set; } - public bool IsHidden { get; internal set; } - public string Description { get; internal set; } - - public IEnumerable Aliases => _aliases; - public IEnumerable Parameters => _parameters; - public CommandParameter this[string name] => _parametersByName[name]; - - internal Command(string text) - { - Text = text; - IsHidden = false; - _aliases = new string[0]; - _parameters = new CommandParameter[0]; - _parametersByName = new Dictionary(); - } - - - internal void SetAliases(string[] aliases) - { - _aliases = aliases; - } - internal void SetParameters(CommandParameter[] parameters) - { - _parametersByName.Clear(); - for (int i = 0; i < parameters.Length; i++) - { - parameters[i].Id = i; - _parametersByName[parameters[i].Name] = parameters[i]; - } - _parameters = parameters; - } - internal void SetChecks(IPermissionChecker[] checks) - { - _checks = checks; - } - - internal bool CanRun(User user, ITextChannel channel, out string error) - { - for (int i = 0; i < _checks.Length; i++) - { - if (!_checks[i].CanRun(this, user, channel, out error)) - return false; - } - error = null; - return true; - } - - internal void SetRunFunc(Func func) - { - _runFunc = func; - } - internal void SetRunFunc(Action func) - { - _runFunc = TaskHelper.ToAsync(func); - } - internal Task Run(CommandEventArgs args) - { - var task = _runFunc(args); - if (task != null) - return task; - else - return TaskHelper.CompletedTask; - } - } -} diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs deleted file mode 100644 index 6b945841c..000000000 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Discord.Commands.Permissions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - //TODO: Make this more friendly and expose it to be extendable - public sealed class CommandBuilder - { - private readonly CommandService _service; - private readonly Command _command; - private readonly List _params; - private readonly List _checks; - private readonly List _aliases; - private readonly string _prefix; - private bool _allowRequiredParams, _areParamsClosed; - - public CommandService Service => _service; - - internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable initialChecks = null) - { - _service = service; - _prefix = prefix; - - _command = new Command(AppendPrefix(prefix, text)); - _command.Category = category; - - if (initialChecks != null) - _checks = new List(initialChecks); - else - _checks = new List(); - - _params = new List(); - _aliases = new List(); - - _allowRequiredParams = true; - _areParamsClosed = false; - } - - public CommandBuilder Alias(params string[] aliases) - { - _aliases.AddRange(aliases); - return this; - } - /*public CommandBuilder Category(string category) - { - _command.Category = category; - return this; - }*/ - public CommandBuilder Description(string description) - { - _command.Description = description; - return this; - } - public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required) - { - if (_areParamsClosed) - throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter."); - if (!_allowRequiredParams && type == ParameterType.Required) - throw new Exception($"{nameof(ParameterType.Required)} parameters may not be added after an optional one"); - - _params.Add(new CommandParameter(name, type)); - - if (type == ParameterType.Optional) - _allowRequiredParams = false; - if (type == ParameterType.Multiple || type == ParameterType.Unparsed) - _areParamsClosed = true; - return this; - } - public CommandBuilder Hide() - { - _command.IsHidden = true; - return this; - } - public CommandBuilder AddCheck(IPermissionChecker check) - { - _checks.Add(check); - return this; - } - public CommandBuilder AddCheck(Func checkFunc, string errorMsg = null) - { - _checks.Add(new GenericPermissionChecker(checkFunc, errorMsg)); - return this; - } - - public void Do(Func func) - { - _command.SetRunFunc(func); - Build(); - } - public void Do(Action func) - { - _command.SetRunFunc(func); - Build(); - } - private void Build() - { - _command.SetParameters(_params.ToArray()); - _command.SetChecks(_checks.ToArray()); - _command.SetAliases(_aliases.Select(x => AppendPrefix(_prefix, x)).ToArray()); - _service.AddCommand(_command); - } - - internal static string AppendPrefix(string prefix, string cmd) - { - if (cmd != "") - { - if (prefix != "") - return prefix + ' ' + cmd; - else - return cmd; - } - else - return prefix; - } - } - public class CommandGroupBuilder - { - private readonly CommandService _service; - private readonly string _prefix; - private readonly List _checks; - private string _category; - - public CommandService Service => _service; - - internal CommandGroupBuilder(CommandService service, string prefix = "", string category = null, IEnumerable initialChecks = null) - { - _service = service; - _prefix = prefix; - _category = category; - if (initialChecks != null) - _checks = new List(initialChecks); - else - _checks = new List(); - } - - public CommandGroupBuilder Category(string category) - { - _category = category; - return this; - } - public void AddCheck(IPermissionChecker checker) - { - _checks.Add(checker); - } - public void AddCheck(Func checkFunc, string errorMsg = null) - { - _checks.Add(new GenericPermissionChecker(checkFunc, errorMsg)); - } - - public CommandGroupBuilder CreateGroup(string cmd, Action config) - { - config(new CommandGroupBuilder(_service, CommandBuilder.AppendPrefix(_prefix, cmd), _category, _checks)); - return this; - } - public CommandBuilder CreateCommand() - => CreateCommand(""); - public CommandBuilder CreateCommand(string cmd) - => new CommandBuilder(_service, cmd, _prefix, _category, _checks); - } -} diff --git a/src/Discord.Net.Commands/CommandErrorEventArgs.cs b/src/Discord.Net.Commands/CommandErrorEventArgs.cs deleted file mode 100644 index 5f47c6d7c..000000000 --- a/src/Discord.Net.Commands/CommandErrorEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput } - public class CommandErrorEventArgs : CommandEventArgs - { - public CommandErrorType ErrorType { get; } - public Exception Exception { get; } - - public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.Args) - { - Exception = ex; - ErrorType = errorType; - } - } -} diff --git a/src/Discord.Net.Commands/CommandEventArgs.cs b/src/Discord.Net.Commands/CommandEventArgs.cs deleted file mode 100644 index 70793f5e1..000000000 --- a/src/Discord.Net.Commands/CommandEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public class CommandEventArgs : EventArgs - { - private readonly string[] _args; - - public Message Message { get; } - public Command Command { get; } - - public User User => Message.User; - public ITextChannel Channel => Message.Channel; - - public CommandEventArgs(Message message, Command command, string[] args) - { - Message = message; - Command = command; - _args = args; - } - - public string[] Args => _args; - public string GetArg(int index) => _args[index]; - public string GetArg(string name) => _args[Command[name].Id]; - } -} diff --git a/src/Discord.Net.Commands/CommandExtensions.cs b/src/Discord.Net.Commands/CommandExtensions.cs deleted file mode 100644 index c57cf099f..000000000 --- a/src/Discord.Net.Commands/CommandExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public static class CommandExtensions - { - public static DiscordClient UsingCommands(this DiscordClient client, CommandServiceConfig config = null) - { - client.AddService(new CommandService(config)); - return client; - } - public static DiscordClient UsingCommands(this DiscordClient client, Action configFunc = null) - { - var builder = new CommandServiceConfigBuilder(); - configFunc(builder); - client.AddService(new CommandService(builder)); - return client; - } - } -} diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs deleted file mode 100644 index ad280b335..000000000 --- a/src/Discord.Net.Commands/CommandMap.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.Commands -{ - //Represents either a single function, command group, or both - internal class CommandMap - { - private readonly CommandMap _parent; - private readonly string _name, _fullName; - - private readonly List _commands; - private readonly Dictionary _items; - private bool _isVisible, _hasNonAliases, _hasSubGroups; - - public string Name => _name; - public string FullName => _fullName; - public bool IsVisible => _isVisible; - public bool HasNonAliases => _hasNonAliases; - public bool HasSubGroups => _hasSubGroups; - public IEnumerable Commands => _commands; - public IEnumerable SubGroups => _items.Values; - - public CommandMap() - { - _items = new Dictionary(); - _commands = new List(); - _isVisible = false; - _hasNonAliases = false; - _hasSubGroups = false; - } - public CommandMap(CommandMap parent, string name, string fullName) - : this() - { - _parent = parent; - _name = name; - _fullName = fullName; - } - - public CommandMap GetItem(string text) - { - return GetItem(0, text.Split(' ')); - } - public CommandMap GetItem(int index, string[] parts) - { - if (index != parts.Length) - { - string nextPart = parts[index]; - CommandMap nextGroup; - if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup)) - return nextGroup.GetItem(index + 1, parts); - else - return null; - } - return this; - } - - public IEnumerable GetCommands() - { - if (_commands.Count > 0) - return _commands; - else if (_parent != null) - return _parent.GetCommands(); - else - return null; - } - public IEnumerable GetCommands(string text) - { - return GetCommands(0, text.Split(' ')); - } - public IEnumerable GetCommands(int index, string[] parts) - { - if (index != parts.Length) - { - string nextPart = parts[index]; - CommandMap nextGroup; - if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup)) - { - var cmd = nextGroup.GetCommands(index + 1, parts); - if (cmd != null) - return cmd; - } - } - - if (_commands != null) - return _commands; - return null; - } - - public void AddCommand(string text, Command command, bool isAlias) - { - AddCommand(0, text.Split(' '), command, isAlias); - } - private void AddCommand(int index, string[] parts, Command command, bool isAlias) - { - if (!command.IsHidden) - _isVisible = true; - - if (index != parts.Length) - { - CommandMap nextGroup; - string name = parts[index].ToLowerInvariant(); - string fullName = string.Join(" ", parts, 0, index + 1); - if (!_items.TryGetValue(name, out nextGroup)) - { - nextGroup = new CommandMap(this, name, fullName); - _items.Add(name, nextGroup); - _hasSubGroups = true; - } - nextGroup.AddCommand(index + 1, parts, command, isAlias); - } - else - { - _commands.Add(command); - if (!isAlias) - _hasNonAliases = true; - } - } - - public bool CanRun(User user, ITextChannel channel, out string error) - { - error = null; - if (_commands.Count > 0) - { - foreach (var cmd in _commands) - { - if (cmd.CanRun(user, channel, out error)) - return true; - } - } - if (_items.Count > 0) - { - foreach (var item in _items) - { - if (item.Value.CanRun(user, channel, out error)) - return true; - } - } - return false; - } - } -} diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs deleted file mode 100644 index d7361bef4..000000000 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Discord.Commands -{ - public enum ParameterType - { - ///

Catches a single required parameter. - Required, - /// Catches a single optional parameter. - Optional, - /// Catches a zero or more optional parameters. - Multiple, - /// Catches all remaining text as a single optional parameter. - Unparsed - } - public class CommandParameter - { - public string Name { get; } - public int Id { get; internal set; } - public ParameterType Type { get; } - - internal CommandParameter(string name, ParameterType type) - { - Name = name; - Type = type; - } - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs deleted file mode 100644 index cfdbe6903..000000000 --- a/src/Discord.Net.Commands/CommandParser.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.Commands -{ - internal static class CommandParser - { - private enum ParserPart - { - None, - Parameter, - QuotedParameter, - DoubleQuotedParameter - } - - public static bool ParseCommand(string input, CommandMap map, out IEnumerable commands, out int endPos) - { - int startPosition = 0; - int endPosition = 0; - int inputLength = input.Length; - bool isEscaped = false; - commands = null; - endPos = 0; - - if (input == "") - return false; - - while (endPosition < inputLength) - { - char currentChar = input[endPosition++]; - if (isEscaped) - isEscaped = false; - else if (currentChar == '\\') - isEscaped = true; - - bool isWhitespace = IsWhiteSpace(currentChar); - if ((!isEscaped && isWhitespace) || endPosition >= inputLength) - { - int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition; - string temp = input.Substring(startPosition, length); - if (temp == "") - startPosition = endPosition; - else - { - var newMap = map.GetItem(temp); - if (newMap != null) - { - map = newMap; - endPos = endPosition; - } - else - break; - startPosition = endPosition; - } - } - } - commands = map.GetCommands(); //Work our way backwards to find a command that matches our input - return commands != null; - } - private static bool IsWhiteSpace(char c) => c == ' ' || c == '\n' || c == '\r' || c == '\t'; - - //TODO: Check support for escaping - public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args) - { - ParserPart currentPart = ParserPart.None; - int startPosition = startPos; - int endPosition = startPos; - int inputLength = input.Length; - bool isEscaped = false; - - var expectedArgs = command._parameters; - List argList = new List(); - CommandParameter parameter = null; - - args = null; - - if (input == "") - return CommandErrorType.InvalidInput; - - while (endPosition < inputLength) - { - if (startPosition == endPosition && (parameter == null || parameter.Type != ParameterType.Multiple)) //Is first char of a new arg - { - if (argList.Count >= expectedArgs.Length) - return CommandErrorType.BadArgCount; //Too many args - parameter = expectedArgs[argList.Count]; - if (parameter.Type == ParameterType.Unparsed) - { - argList.Add(input.Substring(startPosition)); - break; - } - } - - char currentChar = input[endPosition++]; - if (isEscaped) - isEscaped = false; - else if (currentChar == '\\') - isEscaped = true; - - bool isWhitespace = IsWhiteSpace(currentChar); - if (endPosition == startPosition + 1 && isWhitespace) //Has no text yet, and is another whitespace - { - startPosition = endPosition; - continue; - } - - switch (currentPart) - { - case ParserPart.None: - if ((!isEscaped && currentChar == '\"')) - { - currentPart = ParserPart.DoubleQuotedParameter; - startPosition = endPosition; - } - else if ((!isEscaped && currentChar == '\'')) - { - currentPart = ParserPart.QuotedParameter; - startPosition = endPosition; - } - else if ((!isEscaped && isWhitespace) || endPosition >= inputLength) - { - int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition; - if (length == 0) - startPosition = endPosition; - else - { - string temp = input.Substring(startPosition, length); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - } - break; - case ParserPart.QuotedParameter: - if ((!isEscaped && currentChar == '\'')) - { - string temp = input.Substring(startPosition, endPosition - startPosition - 1); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - else if (endPosition >= inputLength) - return CommandErrorType.InvalidInput; - break; - case ParserPart.DoubleQuotedParameter: - if ((!isEscaped && currentChar == '\"')) - { - string temp = input.Substring(startPosition, endPosition - startPosition - 1); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - else if (endPosition >= inputLength) - return CommandErrorType.InvalidInput; - break; - } - } - - //Unclosed quotes - if (currentPart == ParserPart.QuotedParameter || - currentPart == ParserPart.DoubleQuotedParameter) - return CommandErrorType.InvalidInput; - - //Too few args - for (int i = argList.Count; i < expectedArgs.Length; i++) - { - var param = expectedArgs[i]; - switch (param.Type) - { - case ParameterType.Required: - return CommandErrorType.BadArgCount; - case ParameterType.Optional: - case ParameterType.Unparsed: - argList.Add(""); - break; - } - } - - /*if (argList.Count > expectedArgs.Length) - { - if (expectedArgs.Length == 0 || expectedArgs[expectedArgs.Length - 1].Type != ParameterType.Multiple) - return CommandErrorType.BadArgCount; - }*/ - - args = argList.ToArray(); - return null; - } - } -} diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs deleted file mode 100644 index ea530d27b..000000000 --- a/src/Discord.Net.Commands/CommandService.cs +++ /dev/null @@ -1,347 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - public partial class CommandService : IService - { - private readonly List _allCommands; - private readonly Dictionary _categories; - private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing - - public CommandServiceConfig Config { get; } - public CommandGroupBuilder Root { get; } - public DiscordClient Client { get; private set; } - - //AllCommands store a flattened collection of all commands - public IEnumerable AllCommands => _allCommands; - //Groups store all commands by their module, used for more informative help - internal IEnumerable Categories => _categories.Values; - - public event EventHandler CommandExecuted = delegate { }; - public event EventHandler CommandErrored = delegate { }; - - private void OnCommand(CommandEventArgs args) - => CommandExecuted(this, args); - private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) - => CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex)); - - public CommandService() - : this(new CommandServiceConfigBuilder()) - { - } - public CommandService(CommandServiceConfigBuilder builder) - : this(builder.Build()) - { - if (builder.ExecuteHandler != null) - CommandExecuted += builder.ExecuteHandler; - if (builder.ErrorHandler != null) - CommandErrored += builder.ErrorHandler; - } - public CommandService(CommandServiceConfig config) - { - Config = config; - - _allCommands = new List(); - _map = new CommandMap(); - _categories = new Dictionary(); - Root = new CommandGroupBuilder(this); - } - - void IService.Install(DiscordClient client) - { - Client = client; - - if (Config.HelpMode != HelpMode.Disabled) - { - CreateCommand("help") - .Parameter("command", ParameterType.Multiple) - .Hide() - .Description("Returns information about commands.") - .Do(async e => - { - ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false); - if (e.Args.Length > 0) //Show command help - { - var map = _map.GetItem(string.Join(" ", e.Args)); - if (map != null) - await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false); - else - await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false); - } - else //Show general help - await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false); - }); - } - - client.MessageReceived += async (s, e) => - { - if (_allCommands.Count == 0) return; - if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; - - string msg = e.Message.RawText; - if (msg.Length == 0) return; - - string cmdMsg = null; - - //Check for command char - if (Config.PrefixChar.HasValue) - { - if (msg[0] == Config.PrefixChar.Value) - cmdMsg = msg.Substring(1); - } - - //Check for mention - if (cmdMsg == null && Config.AllowMentionPrefix) - { - string mention = client.CurrentUser.Mention; - if (msg.StartsWith(mention) && msg.Length > mention.Length) - cmdMsg = msg.Substring(mention.Length + 1); - else - { - mention = $"@{client.CurrentUser.Name}"; - if (msg.StartsWith(mention) && msg.Length > mention.Length) - cmdMsg = msg.Substring(mention.Length + 1); - } - } - - //Check using custom activator - if (cmdMsg == null && Config.CustomPrefixHandler != null) - { - int index = Config.CustomPrefixHandler(e.Message); - if (index >= 0) - cmdMsg = msg.Substring(index); - } - - if (cmdMsg == null) return; - - //Parse command - IEnumerable commands; - int argPos; - CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos); - if (commands == null) - { - CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null); - OnCommandError(CommandErrorType.UnknownCommand, errorArgs); - return; - } - else - { - foreach (var command in commands) - { - //Parse arguments - string[] args; - var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args); - if (error != null) - { - if (error == CommandErrorType.BadArgCount) - continue; - else - { - var errorArgs = new CommandEventArgs(e.Message, command, null); - OnCommandError(error.Value, errorArgs); - return; - } - } - - var eventArgs = new CommandEventArgs(e.Message, command, args); - - // Check permissions - string errorText; - if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText)) - { - OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null); - return; - } - - // Run the command - try - { - OnCommand(eventArgs); - await command.Run(eventArgs).ConfigureAwait(false); - } - catch (Exception ex) - { - OnCommandError(CommandErrorType.Exception, eventArgs, ex); - } - return; - } - var errorArgs2 = new CommandEventArgs(e.Message, null, null); - OnCommandError(CommandErrorType.BadArgCount, errorArgs2); - } - }; - } - - public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - bool isFirstCategory = true; - foreach (var category in _categories) - { - bool isFirstItem = true; - foreach (var group in category.Value.SubGroups) - { - string error; - if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error)) - { - if (isFirstItem) - { - isFirstItem = false; - //This is called for the first item in each category. If we never get here, we dont bother writing the header for a category type (since it's empty) - if (isFirstCategory) - { - isFirstCategory = false; - //Called for the first non-empty category - output.AppendLine("These are the commands you can use:"); - } - else - output.AppendLine(); - if (category.Key != "") - { - output.Append(Format.Bold(category.Key)); - output.Append(": "); - } - } - else - output.Append(", "); - output.Append('`'); - output.Append(group.Name); - if (group.HasSubGroups) - output.Append("*"); - output.Append('`'); - } - } - } - - if (output.Length == 0) - output.Append("There are no commands you have permission to run."); - else - output.AppendLine("\n\nRun `help ` for more information."); - - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - - private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - - IEnumerable cmds = map.Commands; - bool isFirstCmd = true; - string error; - if (cmds.Any()) - { - foreach (var cmd in cmds) - { - if (cmd.CanRun(user, channel, out error)) - { - if (isFirstCmd) - isFirstCmd = false; - else - output.AppendLine(); - ShowCommandHelpInternal(cmd, user, channel, output); - } - } - } - else - { - output.Append('`'); - output.Append(map.FullName); - output.Append("`\n"); - } - - bool isFirstSubCmd = true; - foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible)) - { - if (isFirstSubCmd) - { - isFirstSubCmd = false; - output.AppendLine("Sub Commands: "); - } - else - output.Append(", "); - output.Append('`'); - output.Append(subCmd.Name); - if (subCmd.SubGroups.Any()) - output.Append("*"); - output.Append('`'); - } - - if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands - { - output.Clear(); - output.AppendLine("There are no commands you have permission to run."); - } - - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - string error; - if (!command.CanRun(user, channel, out error)) - output.AppendLine(error ?? "You do not have permission to access this command."); - else - ShowCommandHelpInternal(command, user, channel, output); - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output) - { - output.Append('`'); - output.Append(command.Text); - foreach (var param in command.Parameters) - { - switch (param.Type) - { - case ParameterType.Required: - output.Append($" <{param.Name}>"); - break; - case ParameterType.Optional: - output.Append($" [{param.Name}]"); - break; - case ParameterType.Multiple: - output.Append($" [{param.Name}...]"); - break; - case ParameterType.Unparsed: - output.Append($" [-]"); - break; - } - } - output.AppendLine("`"); - output.AppendLine($"{command.Description ?? "No description."}"); - - if (command.Aliases.Any()) - output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`'); - } - - public void CreateGroup(string cmd, Action config = null) => Root.CreateGroup(cmd, config); - public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd); - - internal void AddCommand(Command command) - { - _allCommands.Add(command); - - //Get category - CommandMap category; - string categoryName = command.Category ?? ""; - if (!_categories.TryGetValue(categoryName, out category)) - { - category = new CommandMap(); - _categories.Add(categoryName, category); - } - - //Add main command - category.AddCommand(command.Text, command, false); - _map.AddCommand(command.Text, command, false); - - //Add aliases - foreach (var alias in command.Aliases) - { - category.AddCommand(alias, command, true); - _map.AddCommand(alias, command, true); - } - } - } -} diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs deleted file mode 100644 index f43c838fb..000000000 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public class CommandServiceConfigBuilder - { - /// Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. - public char? PrefixChar { get; set; } = null; - /// Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. - public bool AllowMentionPrefix { get; set; } = true; - /// - /// Gets or sets a custom function used to detect messages that should be treated as commands. - /// This function should a positive one indicating the index of where the in the message's RawText the command begins, - /// and a negative value if the message should be ignored. - /// - public Func CustomPrefixHandler { get; set; } = null; - - /// Gets or sets whether a help function should be automatically generated. - public HelpMode HelpMode { get; set; } = HelpMode.Disabled; - - - /// Gets or sets a handler that is called on any successful command execution. - public EventHandler ExecuteHandler { get; set; } - /// Gets or sets a handler that is called on any error during command parsing or execution. - public EventHandler ErrorHandler { get; set; } - - public CommandServiceConfig Build() => new CommandServiceConfig(this); - } - public class CommandServiceConfig - { - public char? PrefixChar { get; } - public bool AllowMentionPrefix { get; } - public Func CustomPrefixHandler { get; } - - /// Gets or sets whether a help function should be automatically generated. - public HelpMode HelpMode { get; set; } = HelpMode.Disabled; - - internal CommandServiceConfig(CommandServiceConfigBuilder builder) - { - PrefixChar = builder.PrefixChar; - AllowMentionPrefix = builder.AllowMentionPrefix; - CustomPrefixHandler = builder.CustomPrefixHandler; - HelpMode = builder.HelpMode; - } - } -} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.xproj b/src/Discord.Net.Commands/Discord.Net.Commands.xproj deleted file mode 100644 index 6c0d0ca91..000000000 --- a/src/Discord.Net.Commands/Discord.Net.Commands.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 19793545-ef89-48f4-8100-3ebaad0a9141 - Discord.Commands - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Commands/GenericPermissionChecker.cs b/src/Discord.Net.Commands/GenericPermissionChecker.cs deleted file mode 100644 index 10d665811..000000000 --- a/src/Discord.Net.Commands/GenericPermissionChecker.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Discord.Commands.Permissions -{ - internal class GenericPermissionChecker : IPermissionChecker - { - private readonly Func _checkFunc; - private readonly string _error; - - public GenericPermissionChecker(Func checkFunc, string error = null) - { - _checkFunc = checkFunc; - _error = error; - } - - public bool CanRun(Command command, User user, ITextChannel channel, out string error) - { - error = _error; - return _checkFunc(command, user, channel); - } - } -} diff --git a/src/Discord.Net.Commands/HelpMode.cs b/src/Discord.Net.Commands/HelpMode.cs deleted file mode 100644 index 272403f42..000000000 --- a/src/Discord.Net.Commands/HelpMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Discord.Commands -{ - public enum HelpMode - { - /// Disable the automatic help command. - Disabled, - /// Use the automatic help command and respond in the channel the command is used. - Public, - /// Use the automatic help command and respond in a private message. - Private - } -} diff --git a/src/Discord.Net.Commands/IPermissionChecker.cs b/src/Discord.Net.Commands/IPermissionChecker.cs deleted file mode 100644 index 0f317ffef..000000000 --- a/src/Discord.Net.Commands/IPermissionChecker.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Commands.Permissions -{ - public interface IPermissionChecker - { - bool CanRun(Command command, User user, ITextChannel channel, out string error); - } -} diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json deleted file mode 100644 index 124a29dfe..000000000 --- a/src/Discord.Net.Commands/project.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": "1.0.0-alpha1", - "description": "A Discord.Net extension adding basic command support.", - "authors": [ "RogueException" ], - "tags": [ "discord", "discordapp" ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "warningsAsErrors": true - }, - - "dependencies": { - "Discord.Net": "1.0.0-alpha1" - }, - "frameworks": { - "net45": { }, - "dotnet5.4": { } - } -} diff --git a/src/Discord.Net.Modules/Discord.Net.Modules.xproj b/src/Discord.Net.Modules/Discord.Net.Modules.xproj deleted file mode 100644 index 77112cd5d..000000000 --- a/src/Discord.Net.Modules/Discord.Net.Modules.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 01584e8a-78da-486f-9ef9-a894e435841b - Discord.Modules - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Modules/IModule.cs b/src/Discord.Net.Modules/IModule.cs deleted file mode 100644 index 48a594eef..000000000 --- a/src/Discord.Net.Modules/IModule.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Modules -{ - public interface IModule - { - void Install(ModuleManager manager); - } -} diff --git a/src/Discord.Net.Modules/ModuleChecker.cs b/src/Discord.Net.Modules/ModuleChecker.cs deleted file mode 100644 index 5f9b8e116..000000000 --- a/src/Discord.Net.Modules/ModuleChecker.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord.Commands; -using Discord.Commands.Permissions; - -namespace Discord.Modules -{ - public class ModuleChecker : IPermissionChecker - { - private readonly ModuleManager _manager; - private readonly ModuleFilter _filterType; - - internal ModuleChecker(ModuleManager manager) - { - _manager = manager; - _filterType = manager.FilterType; - } - - public bool CanRun(Command command, User user, ITextChannel channel, out string error) - { - if (_filterType == ModuleFilter.None || - _filterType == ModuleFilter.AlwaysAllowPrivate || - (channel.IsPublic && _manager.HasChannel(channel))) - { - error = null; - return true; - } - else - { - error = "This module is currently disabled."; - return false; - } - } - } -} diff --git a/src/Discord.Net.Modules/ModuleExtensions.cs b/src/Discord.Net.Modules/ModuleExtensions.cs deleted file mode 100644 index a96517c06..000000000 --- a/src/Discord.Net.Modules/ModuleExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Discord.Modules -{ - public static class ModuleExtensions - { - public static DiscordClient UsingModules(this DiscordClient client) - { - client.AddService(new ModuleService()); - return client; - } - - public static void AddModule(this DiscordClient client, IModule instance, string name = null, ModuleFilter filter = ModuleFilter.None) - { - client.GetService().Add(instance, name, filter); - } - public static void AddModule(this DiscordClient client, string name = null, ModuleFilter filter = ModuleFilter.None) - where T : class, IModule, new() - { - client.GetService().Add(name, filter); - } - public static void AddModule(this DiscordClient client, T instance, string name = null, ModuleFilter filter = ModuleFilter.None) - where T : class, IModule - { - client.GetService().Add(instance, name, filter); - } - public static ModuleManager GetModule(this DiscordClient client) - where T : class, IModule - => client.GetService().Get(); - } -} diff --git a/src/Discord.Net.Modules/ModuleFilter.cs b/src/Discord.Net.Modules/ModuleFilter.cs deleted file mode 100644 index 08fa09a5d..000000000 --- a/src/Discord.Net.Modules/ModuleFilter.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord.Modules -{ - [Flags] - public enum ModuleFilter - { - /// Disables the event and command filters. - None = 0x0, - /// Uses the server whitelist to filter events and commands. - ServerWhitelist = 0x1, - /// Uses the channel whitelist to filter events and commands. - ChannelWhitelist = 0x2, - /// Enables this module in all private messages. - AlwaysAllowPrivate = 0x4 - } -} diff --git a/src/Discord.Net.Modules/ModuleManager.cs b/src/Discord.Net.Modules/ModuleManager.cs deleted file mode 100644 index 71b5b0c08..000000000 --- a/src/Discord.Net.Modules/ModuleManager.cs +++ /dev/null @@ -1,278 +0,0 @@ -using Discord.Commands; -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.Modules -{ - public class ModuleManager : ModuleManager - where T : class, IModule - { - public new T Instance => base.Instance as T; - - internal ModuleManager(DiscordClient client, T instance, string name, ModuleFilter filterType) - : base(client, instance, name, filterType) - { - } - } - - public class ModuleManager - { - public event EventHandler JoinedServer = delegate { }; - public event EventHandler LeftServer = delegate { }; - public event EventHandler ServerUpdated = delegate { }; - public event EventHandler ServerUnavailable = delegate { }; - public event EventHandler ServerAvailable = delegate { }; - - public event EventHandler ChannelCreated = delegate { }; - public event EventHandler ChannelDestroyed = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; - - public event EventHandler RoleCreated = delegate { }; - public event EventHandler RoleUpdated = delegate { }; - public event EventHandler RoleDeleted = delegate { }; - - public event EventHandler UserBanned = delegate { }; - public event EventHandler UserJoined = delegate { }; - public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserUnbanned = delegate { }; - public event EventHandler UserIsTyping = delegate { }; - - public event EventHandler MessageReceived = delegate { }; - public event EventHandler MessageSent = delegate { }; - public event EventHandler MessageDeleted = delegate { }; - public event EventHandler MessageUpdated = delegate { }; - public event EventHandler MessageReadRemotely = delegate { }; - - private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate; - private readonly ConcurrentDictionary _enabledServers; - private readonly ConcurrentDictionary _enabledChannels; - private readonly ConcurrentDictionary _indirectServers; - private readonly AsyncLock _lock; - - public DiscordClient Client { get; } - public IModule Instance { get; } - public string Name { get; } - public string Id { get; } - public ModuleFilter FilterType { get; } - - public IEnumerable EnabledServers => _enabledServers.Select(x => x.Value); - public IEnumerable EnabledChannels => _enabledChannels.Select(x => x.Value); - - internal ModuleManager(DiscordClient client, IModule instance, string name, ModuleFilter filterType) - { - Client = client; - Instance = instance; - Name = name; - FilterType = filterType; - - Id = name.ToLowerInvariant(); - _lock = new AsyncLock(); - - _allowAll = filterType == ModuleFilter.None; - _useServerWhitelist = filterType.HasFlag(ModuleFilter.ServerWhitelist); - _useChannelWhitelist = filterType.HasFlag(ModuleFilter.ChannelWhitelist); - _allowPrivate = filterType.HasFlag(ModuleFilter.AlwaysAllowPrivate); - - _enabledServers = new ConcurrentDictionary(); - _enabledChannels = new ConcurrentDictionary(); - _indirectServers = new ConcurrentDictionary(); - - if (_allowAll || _useServerWhitelist) //Server-only events - { - client.ChannelCreated += (s, e) => - { - var server = (e.Channel as PublicChannel)?.Server; - if (HasServer(server)) - ChannelCreated(s, e); - }; - //TODO: This *is* a channel update if the before/after voice channel is whitelisted - //client.UserVoiceStateUpdated += (s, e) => { if (HasServer(e.Server)) UserVoiceStateUpdated(s, e); }; - } - - client.ChannelDestroyed += (s, e) => { if (HasChannel(e.Channel)) ChannelDestroyed(s, e); }; - client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); }; - - client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); }; - client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); }; - client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); }; - client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); }; - client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); }; - - client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); }; - client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); }; - client.RoleDeleted += (s, e) => { if (HasIndirectServer(e.Server)) RoleDeleted(s, e); }; - - client.JoinedServer += (s, e) => { if (_allowAll) JoinedServer(s, e); }; - client.LeftServer += (s, e) => { if (HasIndirectServer(e.Server)) LeftServer(s, e); }; - client.ServerUpdated += (s, e) => { if (HasIndirectServer(e.After)) ServerUpdated(s, e); }; - client.ServerUnavailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerUnavailable(s, e); }; - client.ServerAvailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerAvailable(s, e); }; - - client.UserJoined += (s, e) => { if (HasIndirectServer(e.Server)) UserJoined(s, e); }; - client.UserLeft += (s, e) => { if (HasIndirectServer(e.Server)) UserLeft(s, e); }; - //TODO: We aren't getting events from UserPresence if AllowPrivate is enabled, but the server we know that user through isn't on the whitelist - client.UserUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserUpdated(s, e); }; - client.UserIsTyping += (s, e) => { if (HasChannel(e.Channel)) UserIsTyping(s, e); }; - client.UserBanned += (s, e) => { if (HasIndirectServer(e.Server)) UserBanned(s, e); }; - client.UserUnbanned += (s, e) => { if (HasIndirectServer(e.Server)) UserUnbanned(s, e); }; - } - - public void CreateCommands(string prefix, Action config) - { - var commandService = Client.GetService(); - commandService.CreateGroup(prefix, x => - { - x.Category(Name); - x.AddCheck(new ModuleChecker(this)); - config(x); - }); - - } - public bool EnableServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - - using (_lock.Lock()) - return EnableServerInternal(server); - } - public void EnableServers(IEnumerable servers) - { - if (servers == null) throw new ArgumentNullException(nameof(servers)); - if (servers.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(servers)); - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - - using (_lock.Lock()) - { - foreach (var server in servers) - EnableServerInternal(server); - } - } - private bool EnableServerInternal(Server server) => _enabledServers.TryAdd(server.Id, server); - - public bool DisableServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (!_useServerWhitelist) return false; - - using (_lock.Lock()) - return _enabledServers.TryRemove(server.Id, out server); - } - public void DisableAllServers() - { - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - if (!_useServerWhitelist) return; - - using (_lock.Lock()) - _enabledServers.Clear(); - } - - public bool EnableChannel(ITextChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist."); - - using (_lock.Lock()) - return EnableChannelInternal(channel); - } - public void EnableChannels(IEnumerable channels) - { - if (channels == null) throw new ArgumentNullException(nameof(channels)); - if (channels.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(channels)); - if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist."); - - using (_lock.Lock()) - { - foreach (var channel in channels) - EnableChannelInternal(channel); - } - } - private bool EnableChannelInternal(ITextChannel channel) - { - if (_enabledChannels.TryAdd(channel.Id, channel)) - { - if (channel.Type != ChannelType.Private) - { - var server = (channel as PublicChannel)?.Server; - int value = 0; - _indirectServers.TryGetValue(server.Id, out value); - value++; - _indirectServers[server.Id] = value; - } - return true; - } - return false; - } - - public bool DisableChannel(IChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (!_useChannelWhitelist) return false; - - IChannel ignored; - if (_enabledChannels.TryRemove(channel.Id, out ignored)) - { - using (_lock.Lock()) - { - if (channel.Type != ChannelType.Private) - { - var server = (channel as PublicChannel)?.Server; - int value = 0; - _indirectServers.TryGetValue(server.Id, out value); - value--; - if (value <= 0) - _indirectServers.TryRemove(server.Id, out value); - else - _indirectServers[server.Id] = value; - } - return true; - } - } - return false; - } - public void DisableAllChannels() - { - if (!_useChannelWhitelist) return; - - using (_lock.Lock()) - { - _enabledChannels.Clear(); - _indirectServers.Clear(); - } - } - - public void DisableAll() - { - if (_useServerWhitelist) - DisableAllServers(); - if (_useChannelWhitelist) - DisableAllChannels(); - } - - internal bool HasServer(Server server) => - _allowAll || - (_useServerWhitelist && _enabledServers.ContainsKey(server.Id)); - internal bool HasIndirectServer(Server server) => - _allowAll || - (_useServerWhitelist && _enabledServers.ContainsKey(server.Id)) || - (_useChannelWhitelist && _indirectServers.ContainsKey(server.Id)); - internal bool HasChannel(IChannel channel) - { - if (_allowAll) return true; - if (channel.Type == ChannelType.Private) return _allowPrivate; - - if (_useChannelWhitelist && _enabledChannels.ContainsKey(channel.Id)) return true; - if (_useServerWhitelist && channel.IsPublic) - { - var server = (channel as PublicChannel).Server; - if (server == null) return false; - if (_enabledServers.ContainsKey(server.Id)) return true; - } - return false; - } - } -} diff --git a/src/Discord.Net.Modules/ModuleService.cs b/src/Discord.Net.Modules/ModuleService.cs deleted file mode 100644 index 1f405a222..000000000 --- a/src/Discord.Net.Modules/ModuleService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Discord.Modules -{ - public class ModuleService : IService - { - public DiscordClient Client { get; private set; } - - private static readonly MethodInfo addMethod = typeof(ModuleService).GetTypeInfo().GetDeclaredMethods(nameof(Add)) - .Single(x => x.IsGenericMethodDefinition && x.GetParameters().Length == 3); - - public IEnumerable Modules => _modules.Values; - private readonly Dictionary _modules; - - public ModuleService() - { - _modules = new Dictionary(); - } - - void IService.Install(DiscordClient client) - { - Client = client; - } - - public void Add(IModule instance, string name, ModuleFilter filter) - { - Type type = instance.GetType(); - addMethod.MakeGenericMethod(type).Invoke(this, new object[] { instance, name, filter }); - } - public void Add(string name, ModuleFilter filter) - where T : class, IModule, new() - => Add(new T(), name, filter); - public void Add(T instance, string name, ModuleFilter filter) - where T : class, IModule - { - if (instance == null) throw new ArgumentNullException(nameof(instance)); - if (Client == null) - throw new InvalidOperationException("Service needs to be added to a DiscordClient before modules can be installed."); - - Type type = typeof(T); - if (name == null) name = type.Name; - if (_modules.ContainsKey(type)) - throw new InvalidOperationException("This module has already been added."); - - var manager = new ModuleManager(Client, instance, name, filter); - _modules.Add(type, manager); - instance.Install(manager); - } - public ModuleManager Get() - where T : class, IModule - { - ModuleManager manager; - if (_modules.TryGetValue(typeof(T), out manager)) - return manager as ModuleManager; - return null; - } - } -} diff --git a/src/Discord.Net.Modules/project.json b/src/Discord.Net.Modules/project.json deleted file mode 100644 index e8e897cac..000000000 --- a/src/Discord.Net.Modules/project.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "1.0.0-alpha1", - "description": "A Discord.Net extension adding basic plugin support.", - "authors": [ "RogueException" ], - "tags": [ "discord", "discordapp" ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "warningsAsErrors": true - }, - - "dependencies": { - "Discord.Net": "1.0.0-alpha1", - "Discord.Net.Commands": "1.0.0-alpha1" - }, - "frameworks": { - "net45": { }, - "dotnet5.4": { } - } -} diff --git a/src/Discord.Net.Shared/Discord.Net.Shared.projitems b/src/Discord.Net.Shared/Discord.Net.Shared.projitems deleted file mode 100644 index 38e4eafc8..000000000 --- a/src/Discord.Net.Shared/Discord.Net.Shared.projitems +++ /dev/null @@ -1,16 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2875deb5-f248-4105-8ea2-5141e3de8025 - - - Discord.Net.Shared - - - - - - - \ No newline at end of file diff --git a/src/Discord.Net.Shared/Discord.Net.Shared.shproj b/src/Discord.Net.Shared/Discord.Net.Shared.shproj deleted file mode 100644 index 5d4d53951..000000000 --- a/src/Discord.Net.Shared/Discord.Net.Shared.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2875deb5-f248-4105-8ea2-5141e3de8025 - 14.0 - - - - - - - - diff --git a/src/Discord.Net.Shared/EpochTime.cs b/src/Discord.Net.Shared/EpochTime.cs deleted file mode 100644 index b4dd03fe9..000000000 --- a/src/Discord.Net.Shared/EpochTime.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - internal class EpochTime - { - private const long epoch = 621355968000000000L; //1/1/1970 in Ticks - - public static long GetMilliseconds() => (DateTime.UtcNow.Ticks - epoch) / TimeSpan.TicksPerMillisecond; - } -} diff --git a/src/Discord.Net.Shared/TaskExtensions.cs b/src/Discord.Net.Shared/TaskExtensions.cs deleted file mode 100644 index 520114f02..000000000 --- a/src/Discord.Net.Shared/TaskExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - internal static class TaskExtensions - { - public static async Task Timeout(this Task task, int milliseconds) - { - Task timeoutTask = Task.Delay(milliseconds); - Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (finishedTask == timeoutTask) - throw new TimeoutException(); - else - await task.ConfigureAwait(false); - } - public static async Task Timeout(this Task task, int milliseconds) - { - Task timeoutTask = Task.Delay(milliseconds); - Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (finishedTask == timeoutTask) - throw new TimeoutException(); - else - return await task.ConfigureAwait(false); - } - public static async Task Timeout(this Task task, int milliseconds, CancellationTokenSource timeoutToken) - { - try - { - timeoutToken.CancelAfter(milliseconds); - await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (timeoutToken.IsCancellationRequested) - throw new TimeoutException(); - throw; - } - } - public static async Task Timeout(this Task task, int milliseconds, CancellationTokenSource timeoutToken) - { - try - { - timeoutToken.CancelAfter(milliseconds); - return await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (timeoutToken.IsCancellationRequested) - throw new TimeoutException(); - throw; - } - } - - public static async Task Wait(this CancellationTokenSource tokenSource) - { - var token = tokenSource.Token; - try { await Task.Delay(-1, token).ConfigureAwait(false); } - catch (OperationCanceledException) { } //Expected - } - public static async Task Wait(this CancellationToken token) - { - try { await Task.Delay(-1, token).ConfigureAwait(false); } - catch (OperationCanceledException) { } //Expected - } - } -} diff --git a/src/Discord.Net.Shared/TaskHelper.cs b/src/Discord.Net.Shared/TaskHelper.cs deleted file mode 100644 index 83408b8e9..000000000 --- a/src/Discord.Net.Shared/TaskHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - internal static class TaskHelper - { -#if DOTNET54 - public static Task CompletedTask => Task.CompletedTask; -#else - public static Task CompletedTask => Task.Delay(0); -#endif - - public static Func ToAsync(Action action) - { - return () => - { - action(); return CompletedTask; - }; - } - public static Func ToAsync(Action action) - { - return x => - { - action(x); return CompletedTask; - }; - } - } -} diff --git a/src/Discord.Net/API/Client/Common/Channel.cs b/src/Discord.Net/API/Client/Common/Channel.cs deleted file mode 100644 index 37eac8e48..000000000 --- a/src/Discord.Net/API/Client/Common/Channel.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class Channel : ChannelReference - { - public class PermissionOverwrite - { - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("deny")] - public uint Deny { get; set; } - [JsonProperty("allow")] - public uint Allow { get; set; } - } - - [JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? LastMessageId { get; set; } - [JsonProperty("is_private")] - public bool? IsPrivate { get; set; } - [JsonProperty("position")] - public int? Position { get; set; } - [JsonProperty("topic")] - public string Topic { get; set; } - [JsonProperty("permission_overwrites")] - public PermissionOverwrite[] PermissionOverwrites { get; set; } - [JsonProperty("recipient")] - public UserReference Recipient { get; set; } - [JsonProperty("bitrate")] - public int Bitrate { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/ChannelReference.cs b/src/Discord.Net/API/Client/Common/ChannelReference.cs deleted file mode 100644 index a243e0f49..000000000 --- a/src/Discord.Net/API/Client/Common/ChannelReference.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class ChannelReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? GuildId { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("type")] - public string Type { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/ExtendedMember.cs b/src/Discord.Net/API/Client/Common/ExtendedMember.cs deleted file mode 100644 index 890ec9de5..000000000 --- a/src/Discord.Net/API/Client/Common/ExtendedMember.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class ExtendedMember : Member - { - [JsonProperty("mute")] - public bool? IsServerMuted { get; set; } - [JsonProperty("deaf")] - public bool? IsServerDeafened { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Guild.cs b/src/Discord.Net/API/Client/Common/Guild.cs deleted file mode 100644 index 1ee4a0b51..000000000 --- a/src/Discord.Net/API/Client/Common/Guild.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Guild : GuildReference - { - public class EmojiData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] RoleIds { get; set; } - [JsonProperty("require_colons")] - public bool RequireColons { get; set; } - [JsonProperty("managed")] - public bool IsManaged { get; set; } - } - - [JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? AFKChannelId { get; set; } - [JsonProperty("afk_timeout")] - public int? AFKTimeout { get; set; } - [JsonProperty("embed_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? EmbedChannelId { get; set; } - [JsonProperty("embed_enabled")] - public bool EmbedEnabled { get; set; } - [JsonProperty("icon")] - public string Icon { get; set; } - [JsonProperty("joined_at")] - public DateTime? JoinedAt { get; set; } - [JsonProperty("owner_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? OwnerId { get; set; } - [JsonProperty("region")] - public string Region { get; set; } - [JsonProperty("roles")] - public Role[] Roles { get; set; } - [JsonProperty("features")] - public string[] Features { get; set; } - [JsonProperty("emojis")] - public EmojiData[] Emojis { get; set; } - [JsonProperty("splash")] - public string Splash { get; set; } - [JsonProperty("verification_level")] - public int VerificationLevel { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/GuildReference.cs b/src/Discord.Net/API/Client/Common/GuildReference.cs deleted file mode 100644 index 5b87c3cc2..000000000 --- a/src/Discord.Net/API/Client/Common/GuildReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class GuildReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Invite.cs b/src/Discord.Net/API/Client/Common/Invite.cs deleted file mode 100644 index 571f551ee..000000000 --- a/src/Discord.Net/API/Client/Common/Invite.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Invite : InviteReference - { - [JsonProperty("max_age")] - public int? MaxAge { get; set; } - [JsonProperty("max_uses")] - public int? MaxUses { get; set; } - [JsonProperty("revoked")] - public bool? IsRevoked { get; set; } - [JsonProperty("temporary")] - public bool? IsTemporary { get; set; } - [JsonProperty("uses")] - public int? Uses { get; set; } - [JsonProperty("created_at")] - public DateTime? CreatedAt { get; set; } - [JsonProperty("inviter")] - public UserReference Inviter { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/InviteReference.cs b/src/Discord.Net/API/Client/Common/InviteReference.cs deleted file mode 100644 index 194165173..000000000 --- a/src/Discord.Net/API/Client/Common/InviteReference.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class InviteReference - { - public class GuildData : GuildReference - { - [JsonProperty("splash_hash")] - public string Splash { get; set; } - } - - [JsonProperty("guild")] - public GuildData Guild { get; set; } - [JsonProperty("channel")] - public ChannelReference Channel { get; set; } - [JsonProperty("code")] - public string Code { get; set; } - [JsonProperty("xkcdpass")] - public string XkcdPass { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Member.cs b/src/Discord.Net/API/Client/Common/Member.cs deleted file mode 100644 index 1af23c207..000000000 --- a/src/Discord.Net/API/Client/Common/Member.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Member : MemberReference - { - [JsonProperty("joined_at")] - public DateTime? JoinedAt { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] Roles { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/MemberPresence.cs b/src/Discord.Net/API/Client/Common/MemberPresence.cs deleted file mode 100644 index 589ad46c1..000000000 --- a/src/Discord.Net/API/Client/Common/MemberPresence.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MemberPresence : MemberReference - { - public class GameInfo - { - [JsonProperty("name")] - public string Name { get; set; } - } - [JsonProperty("game")] - public GameInfo Game { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] Roles { get; set; } //TODO: Might be temporary - } -} diff --git a/src/Discord.Net/API/Client/Common/MemberReference.cs b/src/Discord.Net/API/Client/Common/MemberReference.cs deleted file mode 100644 index ba6f37762..000000000 --- a/src/Discord.Net/API/Client/Common/MemberReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MemberReference - { - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? GuildId { get; set; } - [JsonProperty("user")] - public UserReference User { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Message.cs b/src/Discord.Net/API/Client/Common/Message.cs deleted file mode 100644 index 7e9271dc9..000000000 --- a/src/Discord.Net/API/Client/Common/Message.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Message : MessageReference - { - public class Attachment - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("proxy_url")] - public string ProxyUrl { get; set; } - [JsonProperty("size")] - public int Size { get; set; } - [JsonProperty("filename")] - public string Filename { get; set; } - [JsonProperty("width")] - public int? Width { get; set; } - [JsonProperty("height")] - public int? Height { get; set; } - } - - public class Embed - { - public class Reference - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } - - public class ThumbnailInfo - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("proxy_url")] - public string ProxyUrl { get; set; } - [JsonProperty("width")] - public int? Width { get; set; } - [JsonProperty("height")] - public int? Height { get; set; } - } - public class VideoInfo - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("width")] - public int? Width { get; set; } - [JsonProperty("height")] - public int? Height { get; set; } - } - - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("title")] - public string Title { get; set; } - [JsonProperty("description")] - public string Description { get; set; } - [JsonProperty("author")] - public Reference Author { get; set; } - [JsonProperty("provider")] - public Reference Provider { get; set; } - [JsonProperty("thumbnail")] - public ThumbnailInfo Thumbnail { get; set; } - [JsonProperty("video")] - public VideoInfo Video { get; set; } - } - - [JsonProperty("tts")] - public bool? IsTextToSpeech { get; set; } - [JsonProperty("mention_everyone")] - public bool? IsMentioningEveryone { get; set; } - [JsonProperty("timestamp")] - public DateTime? Timestamp { get; set; } - [JsonProperty("edited_timestamp")] - public DateTime? EditedTimestamp { get; set; } - [JsonProperty("mentions")] - public UserReference[] Mentions { get; set; } - [JsonProperty("embeds")] - public Embed[] Embeds { get; set; } //TODO: Parse this - [JsonProperty("attachments")] - public Attachment[] Attachments { get; set; } - [JsonProperty("content")] - public string Content { get; set; } - [JsonProperty("author")] - public UserReference Author { get; set; } - [JsonProperty("nonce")] - public string Nonce { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/MessageReference.cs b/src/Discord.Net/API/Client/Common/MessageReference.cs deleted file mode 100644 index c2afa6cd5..000000000 --- a/src/Discord.Net/API/Client/Common/MessageReference.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MessageReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("message_id"), JsonConverter(typeof(LongStringConverter))] //Only used in MESSAGE_ACK - public ulong MessageId { get { return Id; } set { Id = value; } } - [JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))] - public ulong ChannelId { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/RoleReference.cs b/src/Discord.Net/API/Client/Common/RoleReference.cs deleted file mode 100644 index ce7d25fcc..000000000 --- a/src/Discord.Net/API/Client/Common/RoleReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class RoleReference - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role_id"), JsonConverter(typeof(LongStringConverter))] - public ulong RoleId { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/User.cs b/src/Discord.Net/API/Client/Common/User.cs deleted file mode 100644 index 86c6f22f6..000000000 --- a/src/Discord.Net/API/Client/Common/User.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class User : UserReference - { - [JsonProperty("email")] - public string Email { get; set; } - [JsonProperty("verified")] - public bool? IsVerified { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/UserReference.cs b/src/Discord.Net/API/Client/Common/UserReference.cs deleted file mode 100644 index f2223df87..000000000 --- a/src/Discord.Net/API/Client/Common/UserReference.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class UserReference - { - [JsonProperty("username")] - public string Username { get; set; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("discriminator")] - public ushort? Discriminator { get; set; } - [JsonProperty("avatar")] - public string Avatar { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs deleted file mode 100644 index 9f3f9cefb..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - [JsonObject(MemberSerialization.OptIn)] - public class HeartbeatCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; - object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); - bool IWebSocketMessage.IsPrivate => false; - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs deleted file mode 100644 index 06255bdcf..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket.Events -{ - //public class GuildEmojisUpdateEvent { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs deleted file mode 100644 index 0767b2f8f..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - //public class GuildIntegrationsUpdateEvent { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs deleted file mode 100644 index 311186b11..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - public class GuildMemberRemoveEvent : Member { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs deleted file mode 100644 index 9b56a95b0..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - public class GuildMemberUpdateEvent : Member { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs deleted file mode 100644 index 3d8e2f459..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class GuildRoleCreateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role")] - public Role Data { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs deleted file mode 100644 index e26b65c4d..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class GuildRoleUpdateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role")] - public Role Data { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs deleted file mode 100644 index 484cec1bc..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class TypingStartEvent - { - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] - public ulong UserId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))] - public ulong ChannelId { get; set; } - [JsonProperty("timestamp")] - public int Timestamp { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs deleted file mode 100644 index aad938157..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - //public class UserSettingsUpdateEvent { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs deleted file mode 100644 index d305642a1..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class VoiceServerUpdateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("endpoint")] - public string Endpoint { get; set; } - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/ISerializable.cs b/src/Discord.Net/API/Client/ISerializable.cs deleted file mode 100644 index d23dc3c6c..000000000 --- a/src/Discord.Net/API/Client/ISerializable.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.IO; - -namespace Discord.API.Client -{ - public interface ISerializable - { - void Write(BinaryWriter writer); - } -} diff --git a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs b/src/Discord.Net/API/Client/Rest/AcceptInvite.cs deleted file mode 100644 index 2940c98ac..000000000 --- a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class AcceptInviteRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"invite/{InviteId}"; - object IRestRequest.Payload => null; - - public string InviteId { get; set; } - - public AcceptInviteRequest(string inviteId) - { - InviteId = inviteId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs b/src/Discord.Net/API/Client/Rest/AddGuildBan.cs deleted file mode 100644 index 3e0b165f5..000000000 --- a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class AddGuildBanRequest : IRestRequest - { - string IRestRequest.Method => "PUT"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}?delete-message-days={PruneDays}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - - public int PruneDays { get; set; } = 0; - - public AddGuildBanRequest(ulong guildId, ulong userId) - { - GuildId = guildId; - UserId = userId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/DeleteRole.cs b/src/Discord.Net/API/Client/Rest/DeleteRole.cs deleted file mode 100644 index 56faf3d33..000000000 --- a/src/Discord.Net/API/Client/Rest/DeleteRole.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class DeleteRoleRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } - - public DeleteRoleRequest(ulong guildId, ulong roleId) - { - GuildId = guildId; - RoleId = roleId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/Gateway.cs b/src/Discord.Net/API/Client/Rest/Gateway.cs deleted file mode 100644 index 02dd71008..000000000 --- a/src/Discord.Net/API/Client/Rest/Gateway.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GatewayRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"gateway"; - object IRestRequest.Payload => null; - } - - public class GatewayResponse - { - [JsonProperty("url")] - public string Url { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetBans.cs b/src/Discord.Net/API/Client/Rest/GetBans.cs deleted file mode 100644 index 714cdbaf8..000000000 --- a/src/Discord.Net/API/Client/Rest/GetBans.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetBansRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetBansRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetInvite.cs b/src/Discord.Net/API/Client/Rest/GetInvite.cs deleted file mode 100644 index 2531ac26a..000000000 --- a/src/Discord.Net/API/Client/Rest/GetInvite.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetInviteRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"invite/{InviteCode}"; - object IRestRequest.Payload => null; - - public string InviteCode { get; set; } - - public GetInviteRequest(string inviteCode) - { - InviteCode = inviteCode; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetInvites.cs b/src/Discord.Net/API/Client/Rest/GetInvites.cs deleted file mode 100644 index 2b4f2f5fe..000000000 --- a/src/Discord.Net/API/Client/Rest/GetInvites.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetInvitesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetInvitesRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetMessages.cs b/src/Discord.Net/API/Client/Rest/GetMessages.cs deleted file mode 100644 index 1beadb9a9..000000000 --- a/src/Discord.Net/API/Client/Rest/GetMessages.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Newtonsoft.Json; -using System.Text; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetMessagesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint - { - get - { - StringBuilder query = new StringBuilder(); - this.AddQueryParam(query, "limit", Limit.ToString()); - if (RelativeDir != null) - this.AddQueryParam(query, RelativeDir, RelativeId.ToString()); - return $"channels/{ChannelId}/messages{query}"; - } - } - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - - public int Limit { get; set; } = 100; - public string RelativeDir { get; set; } = null; - public ulong RelativeId { get; set; } = 0; - - public GetMessagesRequest(ulong channelId) - { - ChannelId = channelId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetWidget.cs b/src/Discord.Net/API/Client/Rest/GetWidget.cs deleted file mode 100644 index 0437a8b6b..000000000 --- a/src/Discord.Net/API/Client/Rest/GetWidget.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetWidgetRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"servers/{GuildId}/widget.json"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetWidgetRequest(ulong guildId) - { - GuildId = guildId; - } - } - - public class GetWidgetResponse - { - public class Channel - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - } - public class User : UserReference - { - [JsonProperty("avatar_url")] - public string AvatarUrl { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("game")] - public UserGame Game { get; set; } - } - public class UserGame - { - [JsonProperty("id")] - public int Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } - - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("channels")] - public Channel[] Channels { get; set; } - [JsonProperty("members")] - public MemberReference[] Members { get; set; } - [JsonProperty("instant_invite")] - public string InstantInviteUrl { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Rest/KickMember.cs b/src/Discord.Net/API/Client/Rest/KickMember.cs deleted file mode 100644 index 4808f8543..000000000 --- a/src/Discord.Net/API/Client/Rest/KickMember.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class KickMemberRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - - public KickMemberRequest(ulong guildId, ulong userId) - { - GuildId = guildId; - UserId = userId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/Login.cs b/src/Discord.Net/API/Client/Rest/Login.cs deleted file mode 100644 index f9c89c717..000000000 --- a/src/Discord.Net/API/Client/Rest/Login.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class LoginRequest : IRestRequest - { - string IRestRequest.Method => Email != null ? "POST" : "GET"; - string IRestRequest.Endpoint => $"auth/login"; - object IRestRequest.Payload => this; - - [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] - public string Email { get; set; } - [JsonProperty("password", NullValueHandling = NullValueHandling.Ignore)] - public string Password { get; set; } - } - - public class LoginResponse - { - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Rest/Logout.cs b/src/Discord.Net/API/Client/Rest/Logout.cs deleted file mode 100644 index 9f4443c51..000000000 --- a/src/Discord.Net/API/Client/Rest/Logout.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class LogoutRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"auth/logout"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Client/Rest/PruneMembers.cs b/src/Discord.Net/API/Client/Rest/PruneMembers.cs deleted file mode 100644 index e80498bb1..000000000 --- a/src/Discord.Net/API/Client/Rest/PruneMembers.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class PruneMembersRequest : IRestRequest - { - string IRestRequest.Method => IsSimulation ? "GET" : "POST"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/prune?days={Days}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public int Days { get; set; } = 30; - public bool IsSimulation { get; set; } = false; - - public PruneMembersRequest(ulong guildId) - { - GuildId = guildId; - } - } - - public class PruneMembersResponse - { - [JsonProperty("pruned")] - public int Pruned { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs b/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs deleted file mode 100644 index b453cba49..000000000 --- a/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class RemoveChannelPermissionsRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - public ulong TargetId { get; set; } - - public RemoveChannelPermissionsRequest(ulong channelId, ulong targetId) - { - ChannelId = channelId; - TargetId = targetId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs b/src/Discord.Net/API/Client/Rest/ReorderChannels.cs deleted file mode 100644 index c13f8b21c..000000000 --- a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System.Linq; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class ReorderChannelsRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; - object IRestRequest.Payload - { - get - { - int pos = StartPos; - return ChannelIds.Select(x => new Channel(x, pos++)); - } - } - - public class Channel - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - - public Channel(ulong id, int position) - { - Id = id; - Position = position; - } - } - - public ulong GuildId { get; set; } - - public ulong[] ChannelIds { get; set; } = new ulong[0]; - public int StartPos { get; set; } = 0; - - public ReorderChannelsRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs b/src/Discord.Net/API/Client/Rest/ReorderRoles.cs deleted file mode 100644 index 300176a76..000000000 --- a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System.Linq; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class ReorderRolesRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; - object IRestRequest.Payload - { - get - { - int pos = StartPos; - return RoleIds.Select(x => new Role(x, pos++)); - } - } - - public class Role - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - - public Role(ulong id, int pos) - { - Id = id; - Position = pos; - } - } - - public ulong GuildId { get; set; } - - public ulong[] RoleIds { get; set; } = new ulong[0]; - public int StartPos { get; set; } = 0; - - public ReorderRolesRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs b/src/Discord.Net/API/Client/Rest/SendIsTyping.cs deleted file mode 100644 index 4c56da0be..000000000 --- a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class SendIsTypingRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - - public SendIsTypingRequest(ulong channelId) - { - ChannelId = channelId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs b/src/Discord.Net/API/Client/Rest/UpdateGuild.cs deleted file mode 100644 index f36b18d9f..000000000 --- a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class UpdateGuildRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}"; - object IRestRequest.Payload => this; - - public ulong GuildId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("region")] - public string Region { get; set; } - [JsonProperty("icon")] - public string IconBase64 { get; set; } - [JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? AFKChannelId { get; set; } - [JsonProperty("afk_timeout")] - public int AFKTimeout { get; set; } - [JsonProperty("splash")] - public object Splash { get; set; } - - public UpdateGuildRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/UpdateRole.cs b/src/Discord.Net/API/Client/Rest/UpdateRole.cs deleted file mode 100644 index 4bea0b52b..000000000 --- a/src/Discord.Net/API/Client/Rest/UpdateRole.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class UpdateRoleRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; - object IRestRequest.Payload => this; - - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("permissions")] - public uint Permissions { get; set; } - [JsonProperty("hoist")] - public bool IsHoisted { get; set; } - [JsonProperty("color")] - public uint Color { get; set; } - - public UpdateRoleRequest(ulong guildId, ulong roleId) - { - GuildId = guildId; - RoleId = roleId; - } - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs deleted file mode 100644 index 349a8a28b..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.API.Client.VoiceSocket -{ - public class HeartbeatCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; - object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); - bool IWebSocketMessage.IsPrivate => false; - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs deleted file mode 100644 index fbb38b9d0..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.VoiceSocket -{ - public class IdentifyCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Identify; - object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => true; - - [JsonProperty("server_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] - public ulong UserId { get; set; } - [JsonProperty("session_id")] - public string SessionId { get; set; } - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs deleted file mode 100644 index d860efe45..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.VoiceSocket -{ - public class SelectProtocolCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.SelectProtocol; - object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - - public class Data - { - [JsonProperty("address")] - public string Address { get; set; } - [JsonProperty("port")] - public int Port { get; set; } - [JsonProperty("mode")] - public string Mode { get; set; } - } - [JsonProperty("protocol")] - public string Protocol { get; set; } = "udp"; - [JsonProperty("data")] - private Data ProtocolData { get; } = new Data(); - - public string ExternalAddress { get { return ProtocolData.Address; } set { ProtocolData.Address = value; } } - public int ExternalPort { get { return ProtocolData.Port; } set { ProtocolData.Port = value; } } - public string EncryptionMode { get { return ProtocolData.Mode; } set { ProtocolData.Mode = value; } } - } -} diff --git a/src/Discord.Net/API/Common/Attachment.cs b/src/Discord.Net/API/Common/Attachment.cs new file mode 100644 index 000000000..1f2c4b8b7 --- /dev/null +++ b/src/Discord.Net/API/Common/Attachment.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Attachment + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("filename")] + public string Filename { get; set; } + [JsonProperty("size")] + public int Size { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public int? Height { get; set; } + [JsonProperty("width")] + public int? Width { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs new file mode 100644 index 000000000..df71979a8 --- /dev/null +++ b/src/Discord.Net/API/Common/Channel.cs @@ -0,0 +1,37 @@ +#pragma warning disable CA1721 + +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Channel + { + //Shared + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("is_private")] + public bool IsPrivate { get; set; } + [JsonProperty("last_message_id")] + public ulong LastMessageId { get; set; } + + //GuildChannel + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("position")] + public int Position { get; set; } + [JsonProperty("permission_overwrites")] + public Overwrite[] PermissionOverwrites { get; set; } + [JsonProperty("topic")] + public string Topic { get; set; } + [JsonProperty("bitrate")] + public int Bitrate { get; set; } + + //DMChannel + [JsonProperty("recipient")] + public User Recipient { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Embed.cs b/src/Discord.Net/API/Common/Embed.cs new file mode 100644 index 000000000..a75bdb636 --- /dev/null +++ b/src/Discord.Net/API/Common/Embed.cs @@ -0,0 +1,21 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Embed + { + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("thumbnail")] + public EmbedThumbnail Thumbnail { get; set; } + [JsonProperty("provider")] + public EmbedProvider Provider { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/EmbedProvider.cs b/src/Discord.Net/API/Common/EmbedProvider.cs new file mode 100644 index 000000000..22c9cbaeb --- /dev/null +++ b/src/Discord.Net/API/Common/EmbedProvider.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedProvider + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/EmbedThumbnail.cs b/src/Discord.Net/API/Common/EmbedThumbnail.cs new file mode 100644 index 000000000..73fe3472d --- /dev/null +++ b/src/Discord.Net/API/Common/EmbedThumbnail.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedThumbnail + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public int? Height { get; set; } + [JsonProperty("width")] + public int? Width { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Emoji.cs b/src/Discord.Net/API/Common/Emoji.cs new file mode 100644 index 000000000..1787c430c --- /dev/null +++ b/src/Discord.Net/API/Common/Emoji.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Emoji + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + [JsonProperty("require_colons")] + public bool RequireColons { get; set; } + [JsonProperty("managed")] + public bool Managed { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net/API/Common/Guild.cs new file mode 100644 index 000000000..cbd50e390 --- /dev/null +++ b/src/Discord.Net/API/Common/Guild.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Guild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("splash")] + public string Splash { get; set; } + [JsonProperty("owner_id")] + public ulong OwnerId { get; set; } + [JsonProperty("region")] + public string Region { get; set; } + [JsonProperty("afk_channel_id")] + public ulong? AFKChannelId { get; set; } + [JsonProperty("afk_timeout")] + public int AFKTimeout { get; set; } + [JsonProperty("embed_enabled")] + public bool EmbedEnabled { get; set; } + [JsonProperty("embed_channel_id")] + public ulong? EmbedChannelId { get; set; } + [JsonProperty("verification_level")] + public int VerificationLevel { get; set; } + [JsonProperty("roles")] + public Role[] Roles { get; set; } + [JsonProperty("emojis")] + public Emoji[] Emojis { get; set; } + [JsonProperty("features")] + public string[] Features { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/GuildEmbed.cs b/src/Discord.Net/API/Common/GuildEmbed.cs new file mode 100644 index 000000000..9aceaa472 --- /dev/null +++ b/src/Discord.Net/API/Common/GuildEmbed.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class GuildEmbed + { + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs new file mode 100644 index 000000000..c28d47d34 --- /dev/null +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class GuildMember + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + [JsonProperty("joined_at")] + public DateTime?JoinedAt { get; set; } + [JsonProperty("deaf")] + public bool Deaf { get; set; } + [JsonProperty("mute")] + public bool Mute { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Integration.cs b/src/Discord.Net/API/Common/Integration.cs new file mode 100644 index 000000000..9b14a0cd4 --- /dev/null +++ b/src/Discord.Net/API/Common/Integration.cs @@ -0,0 +1,32 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class Integration + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("syncing")] + public bool Syncing { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + [JsonProperty("expire_behavior")] + public ulong ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public ulong ExpireGracePeriod { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("account")] + public IntegrationAccount Account { get; set; } + [JsonProperty("synced_at")] + public DateTime SyncedAt { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/IntegrationAccount.cs b/src/Discord.Net/API/Common/IntegrationAccount.cs new file mode 100644 index 000000000..77645caaa --- /dev/null +++ b/src/Discord.Net/API/Common/IntegrationAccount.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class IntegrationAccount + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Invite.cs b/src/Discord.Net/API/Common/Invite.cs new file mode 100644 index 000000000..276314560 --- /dev/null +++ b/src/Discord.Net/API/Common/Invite.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Invite + { + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("guild")] + public InviteGuild Guild { get; set; } + [JsonProperty("channel")] + public InviteChannel Channel { get; set; } + [JsonProperty("xkcdpass")] + public string XkcdPass { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteChannel.cs b/src/Discord.Net/API/Common/InviteChannel.cs new file mode 100644 index 000000000..545d5fecd --- /dev/null +++ b/src/Discord.Net/API/Common/InviteChannel.cs @@ -0,0 +1,15 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class InviteChannel + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteGuild.cs b/src/Discord.Net/API/Common/InviteGuild.cs new file mode 100644 index 000000000..7800a71ea --- /dev/null +++ b/src/Discord.Net/API/Common/InviteGuild.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class InviteGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("splash_hash")] + public string SplashHash { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteMetadata.cs b/src/Discord.Net/API/Common/InviteMetadata.cs new file mode 100644 index 000000000..55eeebeee --- /dev/null +++ b/src/Discord.Net/API/Common/InviteMetadata.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class InviteMetadata : Invite + { + [JsonProperty("inviter")] + public User Inviter { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + [JsonProperty("max_uses")] + public int MaxUses { get; set; } + [JsonProperty("max_age")] + public int MaxAge { get; set; } + [JsonProperty("temporary")] + public bool Temporary { get; set; } + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs new file mode 100644 index 000000000..666c73652 --- /dev/null +++ b/src/Discord.Net/API/Common/Message.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class Message + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("author")] + public User Author { get; set; } + [JsonProperty("content")] + public string Content { get; set; } + [JsonProperty("timestamp")] + public DateTime Timestamp { get; set; } + [JsonProperty("edited_timestamp")] + public DateTime? EditedTimestamp { get; set; } + [JsonProperty("tts")] + public bool IsTextToSpeech { get; set; } + [JsonProperty("mention_everyone")] + public bool IsMentioningEveryone { get; set; } + [JsonProperty("mentions")] + public User[] Mentions { get; set; } + [JsonProperty("attachments")] + public Attachment[] Attachments { get; set; } + [JsonProperty("embeds")] + public Embed[] Embeds { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/ReadState.cs b/src/Discord.Net/API/Common/ReadState.cs new file mode 100644 index 000000000..6fa0c9b6e --- /dev/null +++ b/src/Discord.Net/API/Common/ReadState.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class ReadState + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public ulong LastMentionId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/Connection.cs b/src/Discord.Net/API/Common/Unconfirmed/Connection.cs new file mode 100644 index 000000000..2c82d6cf2 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/Connection.cs @@ -0,0 +1,19 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Connection + { + [JsonProperty("integrations")] + public Integration[] Integrations { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs b/src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs similarity index 95% rename from src/Discord.Net/API/Client/Common/ExtendedGuild.cs rename to src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs index 63c55eddb..00aaeb7b9 100644 --- a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class ExtendedGuild : Guild { diff --git a/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs b/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs new file mode 100644 index 000000000..f09c12e0c --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class ExtendedMember : GuildMember + { + [JsonProperty("mute")] + public bool? IsMuted { get; set; } + [JsonProperty("deaf")] + public bool? IsDeafened { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs new file mode 100644 index 000000000..a57630ed1 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs @@ -0,0 +1,15 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberPresence : MemberReference + { + [JsonProperty("game")] + public MemberPresenceGame Game { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs new file mode 100644 index 000000000..acd805548 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberPresenceGame + { + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs new file mode 100644 index 000000000..edc41f688 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberReference + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/MemberVoiceState.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs similarity index 55% rename from src/Discord.Net/API/Client/Common/MemberVoiceState.cs rename to src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs index 4aab1774c..b79df1790 100644 --- a/src/Discord.Net/API/Client/Common/MemberVoiceState.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs @@ -1,16 +1,15 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class MemberVoiceState { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("guild_id")] public ulong GuildId { get; set; } - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("user_id")] public ulong UserId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } [JsonProperty("session_id")] public string SessionId { get; set; } @@ -22,10 +21,10 @@ namespace Discord.API.Client [JsonProperty("self_deaf")] public bool? IsSelfDeafened { get; set; } [JsonProperty("mute")] - public bool? IsServerMuted { get; set; } + public bool? IsMuted { get; set; } [JsonProperty("deaf")] - public bool? IsServerDeafened { get; set; } + public bool? IsDeafened { get; set; } [JsonProperty("suppress")] - public bool? IsServerSuppressed { get; set; } + public bool? IsSuppressed { get; set; } } } diff --git a/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs b/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs new file mode 100644 index 000000000..d2c1dd268 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MessageReference + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("message_id")] //Only used in MESSAGE_ACK + public ulong MessageId { get { return Id; } set { Id = value; } } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs b/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs new file mode 100644 index 000000000..f1da83b9e --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Overwrite + { + [JsonProperty("id")] + public ulong TargetId { get; set; } + [JsonProperty("type")] + public PermissionTarget TargetType { get; set; } + [JsonProperty("deny")] + public uint Deny { get; set; } + [JsonProperty("allow")] + public uint Allow { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/Role.cs b/src/Discord.Net/API/Common/Unconfirmed/Role.cs similarity index 77% rename from src/Discord.Net/API/Client/Common/Role.cs rename to src/Discord.Net/API/Common/Unconfirmed/Role.cs index 59431989a..e561ab355 100644 --- a/src/Discord.Net/API/Client/Common/Role.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/Role.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class Role { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("permissions")] public uint? Permissions { get; set; } diff --git a/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs b/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs new file mode 100644 index 000000000..bf516faaa --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class RoleReference + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net/API/Common/User.cs new file mode 100644 index 000000000..c8e566711 --- /dev/null +++ b/src/Discord.Net/API/Common/User.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class User + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("username")] + public string Username { get; set; } + [JsonProperty("discriminator")] + public ushort Discriminator { get; set; } + [JsonProperty("avatar")] + public string Avatar { get; set; } + [JsonProperty("verified")] + public bool IsVerified { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("bot")] + public bool Bot { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/UserGuild.cs b/src/Discord.Net/API/Common/UserGuild.cs new file mode 100644 index 000000000..9b0819395 --- /dev/null +++ b/src/Discord.Net/API/Common/UserGuild.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class UserGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("owner")] + public bool Owner { get; set; } + } +} diff --git a/src/Discord.Net/API/Converters.cs b/src/Discord.Net/API/Converters.cs deleted file mode 100644 index 5d80ca99f..000000000 --- a/src/Discord.Net/API/Converters.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace Discord.API.Converters -{ - public class LongStringConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(ulong); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => ((string)reader.Value).ToId(); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(((ulong)value).ToIdString()); - } - - public class NullableLongStringConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(ulong?); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => ((string)reader.Value).ToNullableId(); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(((ulong?)value).ToIdString()); - } - - /*public class LongStringEnumerableConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List result = new List(); - if (reader.TokenType == JsonToken.StartArray) - { - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - result.Add(IdConvert.ToLong((string)reader.Value)); - reader.Read(); - } - } - return result; - } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - writer.WriteNull(); - else - { - writer.WriteStartArray(); - foreach (var v in (IEnumerable)value) - writer.WriteValue(IdConvert.ToString(v)); - writer.WriteEndArray(); - } - } - }*/ - - internal class LongStringArrayConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var result = new List(); - if (reader.TokenType == JsonToken.StartArray) - { - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - result.Add(((string)reader.Value).ToId()); - reader.Read(); - } - } - return result.ToArray(); - } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - writer.WriteNull(); - else - { - writer.WriteStartArray(); - var a = (ulong[])value; - for (int i = 0; i < a.Length; i++) - writer.WriteValue(a[i].ToIdString()); - writer.WriteEndArray(); - } - } - } -} diff --git a/src/Discord.Net/API/Extensions.cs b/src/Discord.Net/API/Extensions.cs deleted file mode 100644 index 77d25fe4e..000000000 --- a/src/Discord.Net/API/Extensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Text; - -namespace Discord.API -{ - internal static class RestRequestExtensions - { - public static void AddQueryParam(this IRestRequest request, StringBuilder builder, string name, string value) - { - if (builder.Length == 0) - builder.Append('?'); - else - builder.Append('&'); - builder.Append(Uri.EscapeDataString(name)); - builder.Append('='); - builder.Append(Uri.EscapeDataString(value)); - } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs b/src/Discord.Net/API/GatewaySocket/OpCode.cs similarity index 67% rename from src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs rename to src/Discord.Net/API/GatewaySocket/OpCode.cs index 9942c670e..cf8e142ef 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs +++ b/src/Discord.Net/API/GatewaySocket/OpCode.cs @@ -1,6 +1,6 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { - public enum OpCodes : byte + public enum OpCode : byte { /// C←S - Used to send most events. Dispatch = 0, @@ -12,13 +12,15 @@ StatusUpdate = 3, /// C→S - Used to join a particular voice channel. VoiceStateUpdate = 4, - /// C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. + /// C→S - Used to ensure the guild's voice server is alive. Only send this if voice connection fails or suddenly drops. VoiceServerPing = 5, /// C→S - Used to resume a connection after a redirect occurs. Resume = 6, /// C←S - Used to notify a client that they must reconnect to another gateway. - Redirect = 7, - /// C→S - Used to request all members that were withheld by large_threshold - RequestGuildMembers = 8 + Reconnect = 7, + /// C→S - Used to request all members that were withheld by large_threshold. + RequestGuildMembers = 8, + /// C←S - Used to notify the client of an invalid session id. + InvalidSession = 9 } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs new file mode 100644 index 000000000..5dac73ec5 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.GatewaySocket +{ + [JsonObject(MemberSerialization.OptIn)] + public class HeartbeatCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Heartbeat; + object IWebSocketMessage.Payload => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs similarity index 73% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs index 8437f595c..235f615ef 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs @@ -1,21 +1,20 @@ using Newtonsoft.Json; using System.Collections.Generic; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class IdentifyCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Identify; + int IWebSocketMessage.OpCode => (int)OpCode.Identify; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("v")] public int Version { get; set; } [JsonProperty("token")] public string Token { get; set; } [JsonProperty("properties")] - public Dictionary Properties { get; set; } + public IReadOnlyDictionary Properties { get; set; } [JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)] public int LargeThreshold { get; set; } [JsonProperty("compress")] diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs similarity index 53% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs index cc3c93176..c4614284d 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs @@ -1,16 +1,14 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class RequestMembersCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.RequestGuildMembers; + int IWebSocketMessage.OpCode => (int)OpCode.RequestGuildMembers; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringArrayConverter))] + [JsonProperty("guild_id")] public ulong[] GuildId { get; set; } [JsonProperty("query")] public string Query { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs similarity index 69% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs index 15486e577..1525c6b6a 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class ResumeCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Resume; + int IWebSocketMessage.OpCode => (int)OpCode.Resume; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("session_id")] public string SessionId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs similarity index 74% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs index dff18b08c..8aa22ede5 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class UpdateStatusCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.StatusUpdate; + int IWebSocketMessage.OpCode => (int)OpCode.StatusUpdate; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; public class GameInfo { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs index 3ccf92c65..d7befa41e 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs @@ -1,18 +1,16 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class UpdateVoiceCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.VoiceStateUpdate; + int IWebSocketMessage.OpCode => (int)OpCode.VoiceStateUpdate; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("guild_id")] public ulong? GuildId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } [JsonProperty("self_mute")] public bool IsSelfMuted { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs similarity index 52% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs index ca26fecc7..1e2769036 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelCreateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs index 2b61a7d78..91c57d640 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelDeleteEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs index 4565ce1bc..227124291 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelUpdateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs similarity index 56% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs index 7ba24473a..c1149ee15 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildBanAddEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs index a56a98494..5474146a7 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildBanRemoveEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs similarity index 55% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs index 41c1c71c7..f07adaed3 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildCreateEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs similarity index 55% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs index cf824c40e..3408183ad 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildDeleteEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs index a2ce6ddb2..098a2ea3b 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildMemberAddEvent : ExtendedMember { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs new file mode 100644 index 000000000..686af9511 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs @@ -0,0 +1,4 @@ +namespace Discord.API.GatewaySocket +{ + public class GuildMemberRemoveEvent : MemberReference { } +} diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs new file mode 100644 index 000000000..339489f76 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs @@ -0,0 +1,4 @@ +namespace Discord.API.GatewaySocket +{ + public class GuildMemberUpdateEvent : GuildMember { } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs index 4f2d36b8a..72936dd16 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildMembersChunkEvent { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("members")] public ExtendedMember[] Members { get; set; } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs new file mode 100644 index 000000000..2740546dc --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class GuildRoleCreateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs index 2ecd2edc5..4986f6193 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildRoleDeleteEvent : RoleReference { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs new file mode 100644 index 000000000..56c232d06 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class GuildRoleUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs similarity index 52% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs index 8fc0f1350..b292c54e6 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildUpdateEvent : Guild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs similarity index 56% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs index 64c106ef5..c8379d369 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageAckEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs index d6d2ec1cc..6b0e1e024 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageCreateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs index cfc2df7ff..d932960ba 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageDeleteEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs index 23521fd93..39ef7e1ba 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageUpdateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs index c40853336..59d915354 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class PresenceUpdateEvent : MemberPresence { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs similarity index 96% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs index 744e5b4b5..51e0f3c8c 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ReadyEvent { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs similarity index 77% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs index fe9d644d4..180bf574f 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class RedirectEvent { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs similarity index 79% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs index 6a50fbe32..3f98b1f35 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ResumedEvent { diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs new file mode 100644 index 000000000..063011bbb --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class TypingStartEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("timestamp")] + public int Timestamp { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs index 3c366310a..e49f8b292 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class UserUpdateEvent : User { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs new file mode 100644 index 000000000..75936bb93 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class VoiceServerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("endpoint")] + public string Endpoint { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs similarity index 58% rename from src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs index f3ba96b17..c0b99c710 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class VoiceStateUpdateEvent : MemberVoiceState { } } diff --git a/src/Discord.Net/API/IRestRequest.cs b/src/Discord.Net/API/IRestRequest.cs deleted file mode 100644 index af520370d..000000000 --- a/src/Discord.Net/API/IRestRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.IO; - -namespace Discord.API -{ - public interface IRestRequest - { - string Method { get; } - string Endpoint { get; } - object Payload { get; } - } - public interface IRestRequest : IRestRequest - where ResponseT : class - { - } - - public interface IRestFileRequest : IRestRequest - { - string Filename { get; } - Stream Stream { get; } - } - public interface IRestFileRequest : IRestFileRequest, IRestRequest - where ResponseT : class - { - } -} diff --git a/src/Discord.Net/API/Client/IWebSocketMessage.cs b/src/Discord.Net/API/IWebSocketMessage.cs similarity index 92% rename from src/Discord.Net/API/Client/IWebSocketMessage.cs rename to src/Discord.Net/API/IWebSocketMessage.cs index 6f6de535a..06c51bf77 100644 --- a/src/Discord.Net/API/Client/IWebSocketMessage.cs +++ b/src/Discord.Net/API/IWebSocketMessage.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public interface IWebSocketMessage { int OpCode { get; } object Payload { get; } - bool IsPrivate { get; } } public class WebSocketMessage { diff --git a/src/Discord.Net/API/Rest/AcceptInvite.cs b/src/Discord.Net/API/Rest/AcceptInvite.cs new file mode 100644 index 000000000..72350253f --- /dev/null +++ b/src/Discord.Net/API/Rest/AcceptInvite.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class AcceptInviteRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; + object IRestRequest.Payload => null; + + public string InviteCode { get; } + + public AcceptInviteRequest(string inviteCode) + { + InviteCode = inviteCode; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/AckMessage.cs b/src/Discord.Net/API/Rest/AckMessage.cs similarity index 59% rename from src/Discord.Net/API/Client/Rest/AckMessage.cs rename to src/Discord.Net/API/Rest/AckMessage.cs index 4cf238b72..387215624 100644 --- a/src/Discord.Net/API/Client/Rest/AckMessage.cs +++ b/src/Discord.Net/API/Rest/AckMessage.cs @@ -1,19 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class AckMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}/ack"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } - - /*[JsonProperty("manual")] - public bool Manual { get; set; }*/ + public ulong ChannelId { get; } + public ulong MessageId { get; } public AckMessageRequest(ulong channelId, ulong messageId) { diff --git a/src/Discord.Net/API/Rest/BeginGuildPrune.cs b/src/Discord.Net/API/Rest/BeginGuildPrune.cs new file mode 100644 index 000000000..0a35bdc80 --- /dev/null +++ b/src/Discord.Net/API/Rest/BeginGuildPrune.cs @@ -0,0 +1,23 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class BeginGuildPruneRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/prune"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("days")] + public int Days { get; set; } = 30; + + public BeginGuildPruneRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateInvite.cs b/src/Discord.Net/API/Rest/CreateChannelInvite.cs similarity index 62% rename from src/Discord.Net/API/Client/Rest/CreateInvite.cs rename to src/Discord.Net/API/Rest/CreateChannelInvite.cs index 73f15c248..f60232f48 100644 --- a/src/Discord.Net/API/Client/Rest/CreateInvite.cs +++ b/src/Discord.Net/API/Rest/CreateChannelInvite.cs @@ -1,28 +1,27 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class CreateInviteRequest : IRestRequest + public class CreateChannelInviteRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } [JsonProperty("max_age")] - public int MaxAge { get; set; } = 1800; + public int MaxAge { get; set; } = 86400; //24 Hours [JsonProperty("max_uses")] public int MaxUses { get; set; } = 0; [JsonProperty("temporary")] public bool IsTemporary { get; set; } = false; [JsonProperty("xkcdpass")] public bool WithXkcdPass { get; set; } = false; - /*[JsonProperty("validate")] - public bool Validate { get; set; }*/ - public CreateInviteRequest(ulong channelId) + public CreateChannelInviteRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs b/src/Discord.Net/API/Rest/CreateDMChannel.cs similarity index 56% rename from src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs rename to src/Discord.Net/API/Rest/CreateDMChannel.cs index e1087dc36..473ecc8e2 100644 --- a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs +++ b/src/Discord.Net/API/Rest/CreateDMChannel.cs @@ -1,16 +1,16 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class CreatePrivateChannelRequest : IRestRequest + public class CreateDMChannelRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"users/@me/channels"; object IRestRequest.Payload => this; - [JsonProperty("recipient_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("recipient_id")] public ulong RecipientId { get; set; } } } diff --git a/src/Discord.Net/API/Client/Rest/CreateGuild.cs b/src/Discord.Net/API/Rest/CreateGuild.cs similarity index 63% rename from src/Discord.Net/API/Client/Rest/CreateGuild.cs rename to src/Discord.Net/API/Rest/CreateGuild.cs index a18d2bee9..c2c9532e0 100644 --- a/src/Discord.Net/API/Client/Rest/CreateGuild.cs +++ b/src/Discord.Net/API/Rest/CreateGuild.cs @@ -1,6 +1,9 @@ -using Newtonsoft.Json; +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class CreateGuildRequest : IRestRequest @@ -13,7 +16,7 @@ namespace Discord.API.Client.Rest public string Name { get; set; } [JsonProperty("region")] public string Region { get; set; } - [JsonProperty("icon")] - public string IconBase64 { get; set; } + [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + public Stream Icon { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildBan.cs b/src/Discord.Net/API/Rest/CreateGuildBan.cs new file mode 100644 index 000000000..c76122c5f --- /dev/null +++ b/src/Discord.Net/API/Rest/CreateGuildBan.cs @@ -0,0 +1,24 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class CreateGuildBanRequest : IRestRequest + { + string IRestRequest.Method => "PUT"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + [JsonProperty("delete-message-days")] + public int PruneDays { get; set; } = 0; + + public CreateGuildBanRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateChannel.cs b/src/Discord.Net/API/Rest/CreateGuildChannel.cs similarity index 73% rename from src/Discord.Net/API/Client/Rest/CreateChannel.cs rename to src/Discord.Net/API/Rest/CreateGuildChannel.cs index 90d9afec0..839f3cc1d 100644 --- a/src/Discord.Net/API/Client/Rest/CreateChannel.cs +++ b/src/Discord.Net/API/Rest/CreateGuildChannel.cs @@ -1,6 +1,7 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class CreateChannelRequest : IRestRequest @@ -9,12 +10,14 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; object IRestRequest.Payload => this; - public ulong GuildId { get; set; } + public ulong GuildId { get; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("type")] public ChannelType Type { get; set; } + [JsonProperty("bitrate")] + public int Bitrate { get; set; } public CreateChannelRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/CreateGuildIntegration.cs b/src/Discord.Net/API/Rest/CreateGuildIntegration.cs new file mode 100644 index 000000000..7cf48397d --- /dev/null +++ b/src/Discord.Net/API/Rest/CreateGuildIntegration.cs @@ -0,0 +1,25 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class CreateGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("id")] + public ulong IntegrationId { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + + public CreateGuildIntegrationRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateRole.cs b/src/Discord.Net/API/Rest/CreateGuildRole.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/CreateRole.cs rename to src/Discord.Net/API/Rest/CreateGuildRole.cs index 3978c6aaa..0d21805d8 100644 --- a/src/Discord.Net/API/Client/Rest/CreateRole.cs +++ b/src/Discord.Net/API/Rest/CreateGuildRole.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class CreateRoleRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public CreateRoleRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Client/Rest/SendMessage.cs b/src/Discord.Net/API/Rest/CreateMessage.cs similarity index 51% rename from src/Discord.Net/API/Client/Rest/SendMessage.cs rename to src/Discord.Net/API/Rest/CreateMessage.cs index 9caca991d..1018a8c66 100644 --- a/src/Discord.Net/API/Client/Rest/SendMessage.cs +++ b/src/Discord.Net/API/Rest/CreateMessage.cs @@ -1,24 +1,25 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class SendMessageRequest : IRestRequest + public class CreateMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } [JsonProperty("content")] public string Content { get; set; } [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public string Nonce { get; set; } - [JsonProperty("tts")] - public bool IsTTS { get; set; } + public string Nonce { get; set; } = null; + [JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsTTS { get; set; } = false; - public SendMessageRequest(ulong channelId) + public CreateMessageRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs b/src/Discord.Net/API/Rest/DeleteChannel.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/DeleteChannel.cs rename to src/Discord.Net/API/Rest/DeleteChannel.cs index ae56934b5..fa9b10c10 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs +++ b/src/Discord.Net/API/Rest/DeleteChannel.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteChannelRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } public DeleteChannelRequest(ulong channelId) { diff --git a/src/Discord.Net/API/Rest/DeleteChannelPermission.cs b/src/Discord.Net/API/Rest/DeleteChannelPermission.cs new file mode 100644 index 000000000..658388641 --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteChannelPermission.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteChannelPermissionsRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + public ulong TargetId { get; } + + public DeleteChannelPermissionsRequest(ulong channelId, ulong targetId) + { + ChannelId = channelId; + TargetId = targetId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/DeleteGuild.cs b/src/Discord.Net/API/Rest/DeleteGuild.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/DeleteGuild.cs rename to src/Discord.Net/API/Rest/DeleteGuild.cs index 44df5892e..d1a14fbc9 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteGuild.cs +++ b/src/Discord.Net/API/Rest/DeleteGuild.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteGuildRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public DeleteGuildRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs b/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs new file mode 100644 index 000000000..b25418c1a --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + public DeleteGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Rest/DeleteGuildRole.cs b/src/Discord.Net/API/Rest/DeleteGuildRole.cs new file mode 100644 index 000000000..cbb21eec3 --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteGuildRole.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteGuildRoleRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong RoleId { get; } + + public DeleteGuildRoleRequest(ulong guildId, ulong roleId) + { + GuildId = guildId; + RoleId = roleId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs b/src/Discord.Net/API/Rest/DeleteInvite.cs similarity index 56% rename from src/Discord.Net/API/Client/Rest/DeleteInvite.cs rename to src/Discord.Net/API/Rest/DeleteInvite.cs index 4bfe1e0d7..388255862 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs +++ b/src/Discord.Net/API/Rest/DeleteInvite.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteInviteRequest : IRestRequest { string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"invite/{InviteCode}"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; object IRestRequest.Payload => null; - public string InviteCode { get; set; } + public string InviteCode { get; } public DeleteInviteRequest(string inviteCode) { diff --git a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs b/src/Discord.Net/API/Rest/DeleteMessage.cs similarity index 67% rename from src/Discord.Net/API/Client/Rest/DeleteMessage.cs rename to src/Discord.Net/API/Rest/DeleteMessage.cs index 3f781a756..c9d95deba 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs +++ b/src/Discord.Net/API/Rest/DeleteMessage.cs @@ -1,16 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteMessageRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } + public ulong ChannelId { get; } + public ulong MessageId { get; } public DeleteMessageRequest(ulong channelId, ulong messageId) { diff --git a/src/Discord.Net/API/Rest/GetChannel.cs b/src/Discord.Net/API/Rest/GetChannel.cs new file mode 100644 index 000000000..12d258836 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannel.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetChannelRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public GetChannelRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetChannelInvites.cs b/src/Discord.Net/API/Rest/GetChannelInvites.cs new file mode 100644 index 000000000..7532fb64f --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannelInvites.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetChannelInvitesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public GetChannelInvitesRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetChannelMessages.cs b/src/Discord.Net/API/Rest/GetChannelMessages.cs new file mode 100644 index 000000000..a0e6b5afe --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannelMessages.cs @@ -0,0 +1,34 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetChannelMessagesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/messages?limit={Limit}&{RelativeDir}={RelativeId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public Relative RelativeDir { get; set; } + public ulong RelativeId { get; set; } = 0; + + [JsonProperty("limit")] + public int Limit { get; set; } = 100; + + [JsonProperty("before")] + public ulong Before => RelativeId; + private bool ShouldSerializeBefore => RelativeDir == Relative.Before; + + [JsonProperty("after")] + public ulong After => RelativeId; + private bool ShouldSerializeAfter => RelativeDir == Relative.After; + + public GetChannelMessagesRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUser.cs b/src/Discord.Net/API/Rest/GetCurrentUser.cs new file mode 100644 index 000000000..76bc3e838 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUser.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs b/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs new file mode 100644 index 000000000..102affaa7 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserConnectionsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/connections"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs b/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs new file mode 100644 index 000000000..31cc1edeb --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserDMsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/channels"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs b/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs new file mode 100644 index 000000000..e3423c3f8 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserGuildsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/guilds"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetGateway.cs b/src/Discord.Net/API/Rest/GetGateway.cs new file mode 100644 index 000000000..3581f9f00 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGateway.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class GetGatewayRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"gateway"; + object IRestRequest.Payload => null; + } + + public class GetGatewayResponse + { + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuild.cs b/src/Discord.Net/API/Rest/GetGuild.cs new file mode 100644 index 000000000..2e83d261a --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuild.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildBans.cs b/src/Discord.Net/API/Rest/GetGuildBans.cs new file mode 100644 index 000000000..73a02b43e --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildBans.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildBansRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildBansRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildChannels.cs b/src/Discord.Net/API/Rest/GetGuildChannels.cs new file mode 100644 index 000000000..f20e9ec4f --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildChannels.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildChannelsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/channels"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildChannelsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildEmbed.cs b/src/Discord.Net/API/Rest/GetGuildEmbed.cs new file mode 100644 index 000000000..0eb67e680 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildEmbed.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildEmbedRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/embed"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildEmbedRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildIntegrations.cs b/src/Discord.Net/API/Rest/GetGuildIntegrations.cs new file mode 100644 index 000000000..ba2b1ce2d --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildIntegrations.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildIntegrationsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildIntegrationsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildInvites.cs b/src/Discord.Net/API/Rest/GetGuildInvites.cs new file mode 100644 index 000000000..4a20770b1 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildInvites.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildInvitesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildInvitesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildMember.cs b/src/Discord.Net/API/Rest/GetGuildMember.cs new file mode 100644 index 000000000..0dc1d8077 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildMember.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildMemberRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + public GetGuildMemberRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildPruneCount.cs b/src/Discord.Net/API/Rest/GetGuildPruneCount.cs new file mode 100644 index 000000000..469b5fa01 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildPruneCount.cs @@ -0,0 +1,29 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetGuildPruneCountRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/prune"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + [JsonProperty("days")] + public int Days { get; set; } = 30; + + public GetGuildPruneCountRequest(ulong guildId) + { + GuildId = guildId; + } + } + + public class GetGuildPruneCountResponse + { + [JsonProperty("pruned")] + public int Pruned { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildRoles.cs b/src/Discord.Net/API/Rest/GetGuildRoles.cs new file mode 100644 index 000000000..f8ff0a9b3 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildRoles.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildRolesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/roles"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildRolesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs b/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs new file mode 100644 index 000000000..ed631e7e2 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildVoiceRegionsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/regions"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildVoiceRegionsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetInvite.cs b/src/Discord.Net/API/Rest/GetInvite.cs new file mode 100644 index 000000000..fccbc79c9 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetInvite.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetInviteRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; + object IRestRequest.Payload => null; + + public string InviteCode { get; } + + public GetInviteRequest(string inviteCode) + { + InviteCode = inviteCode; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetUser.cs b/src/Discord.Net/API/Rest/GetUser.cs new file mode 100644 index 000000000..1cfeb82be --- /dev/null +++ b/src/Discord.Net/API/Rest/GetUser.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/{UserId}"; + object IRestRequest.Payload => null; + + public ulong UserId { get; } + + public GetUserRequest(ulong userId) + { + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs b/src/Discord.Net/API/Rest/GetVoiceRegions.cs similarity index 65% rename from src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs rename to src/Discord.Net/API/Rest/GetVoiceRegions.cs index df21cc203..5fdbccfd8 100644 --- a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs +++ b/src/Discord.Net/API/Rest/GetVoiceRegions.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class GetVoiceRegionsRequest : IRestRequest { string IRestRequest.Method => "GET"; @@ -12,15 +12,17 @@ namespace Discord.API.Client.Rest public class GetVoiceRegionsResponse { - [JsonProperty("sample_hostname")] - public string Hostname { get; set; } - [JsonProperty("sample_port")] - public int Port { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("vip")] - public bool Vip { get; set; } + public bool IsVip { get; set; } + [JsonProperty("optimal")] + public bool IsOptimal { get; set; } + [JsonProperty("sample_hostname")] + public string SampleHostname { get; set; } + [JsonProperty("sample_port")] + public int SamplePort { get; set; } } } diff --git a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs b/src/Discord.Net/API/Rest/LeaveGuild.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/LeaveGuild.cs rename to src/Discord.Net/API/Rest/LeaveGuild.cs index 99fd8cbe7..a62fdd089 100644 --- a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs +++ b/src/Discord.Net/API/Rest/LeaveGuild.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class LeaveGuildRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"users/@me/guilds/{GuildId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public LeaveGuildRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/ListGuildMembers.cs b/src/Discord.Net/API/Rest/ListGuildMembers.cs new file mode 100644 index 000000000..4577b8910 --- /dev/null +++ b/src/Discord.Net/API/Rest/ListGuildMembers.cs @@ -0,0 +1,24 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class ListGuildMembersRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/members"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + [JsonProperty("limit")] + public int Limit { get; } = 1; + [JsonProperty("offset")] + public int Offset { get; } = 0; + + public ListGuildMembersRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs b/src/Discord.Net/API/Rest/ModifyChannelPermission.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/AddChannelPermission.cs rename to src/Discord.Net/API/Rest/ModifyChannelPermission.cs index bf725bcaf..e38685bb7 100644 --- a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs +++ b/src/Discord.Net/API/Rest/ModifyChannelPermission.cs @@ -1,29 +1,27 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class AddOrUpdateChannelPermissionsRequest : IRestRequest + public class ModifyChannelPermissionsRequest : IRestRequest { string IRestRequest.Method => "PUT"; string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } + public ulong TargetId { get; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong TargetId { get; set; } - [JsonProperty("type")] - public string TargetType { get; set; } [JsonProperty("allow")] public uint Allow { get; set; } [JsonProperty("deny")] public uint Deny { get; set; } - public AddOrUpdateChannelPermissionsRequest(ulong channelId) + public ModifyChannelPermissionsRequest(ulong channelId, ulong targetId) { ChannelId = channelId; + TargetId = targetId; } } } diff --git a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs b/src/Discord.Net/API/Rest/ModifyCurrentUser.cs similarity index 57% rename from src/Discord.Net/API/Client/Rest/UpdateProfile.cs rename to src/Discord.Net/API/Rest/ModifyCurrentUser.cs index 0f0cdb313..afc1c3b74 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs +++ b/src/Discord.Net/API/Rest/ModifyCurrentUser.cs @@ -1,23 +1,26 @@ -using Newtonsoft.Json; +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class UpdateProfileRequest : IRestRequest + public class ModifyCurrentUserRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"users/@me"; object IRestRequest.Payload => this; - [JsonProperty("password")] - public string CurrentPassword { get; set; } + [JsonProperty("username")] + public string Username { get; set; } [JsonProperty("email")] public string Email { get; set; } - [JsonProperty("new_password")] + [JsonProperty("password")] public string Password { get; set; } - [JsonProperty("username")] - public string Username { get; set; } - [JsonProperty("avatar")] - public string AvatarBase64 { get; set; } + [JsonProperty("new_password")] + public string NewPassword { get; set; } + [JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] + public Stream Avatar { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuild.cs b/src/Discord.Net/API/Rest/ModifyGuild.cs new file mode 100644 index 000000000..1405a8144 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuild.cs @@ -0,0 +1,39 @@ +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("region")] + public VoiceRegion Region { get; set; } + [JsonProperty("verification_level")] + public int VerificationLevel { get; set; } + [JsonProperty("afk_channel_id")] + public ulong? AFKChannelId { get; set; } + [JsonProperty("afk_timeout")] + public int AFKTimeout { get; set; } + [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + public Stream Icon { get; set; } + [JsonProperty("owner_id")] + public GuildPresence Owner { get; set; } + [JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] + public Stream Splash { get; set; } + + public ModifyGuildRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs b/src/Discord.Net/API/Rest/ModifyGuildChannel.cs similarity index 59% rename from src/Discord.Net/API/Client/Rest/UpdateChannel.cs rename to src/Discord.Net/API/Rest/ModifyGuildChannel.cs index 8a82caefd..eeb909e84 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildChannel.cs @@ -1,9 +1,10 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class UpdateChannelRequest : IRestRequest + public class ModifyGuildChannelRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; @@ -13,14 +14,10 @@ namespace Discord.API.Client.Rest [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("topic")] - public string Topic { get; set; } [JsonProperty("position")] public int Position { get; set; } - [JsonProperty("bitrate")] - public int Bitrate { get; set; } - public UpdateChannelRequest(ulong channelId) + public ModifyGuildChannelRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannels.cs b/src/Discord.Net/API/Rest/ModifyGuildChannels.cs new file mode 100644 index 000000000..de9b97b88 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildChannels.cs @@ -0,0 +1,22 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rest +{ + public class ModifyGuildChannelsRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; + object IRestRequest.Payload => Requests; + + public ulong GuildId { get; } + + public ModifyGuildChannelRequest[] Requests { get; set; } = Array.Empty(); + + public ModifyGuildChannelsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs b/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs new file mode 100644 index 000000000..c896e5a34 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs @@ -0,0 +1,25 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildEmbedRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/embed"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + + public ModifyGuildEmbedRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs b/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs new file mode 100644 index 000000000..b093888da --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs @@ -0,0 +1,29 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + [JsonProperty("expire_behavior")] + public int ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public int ExpireGracePeriod { get; set; } + [JsonProperty("enable_emoticons")] + public bool EnableEmoticons { get; set; } + + public ModifyGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateMember.cs b/src/Discord.Net/API/Rest/ModifyGuildMember.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/UpdateMember.cs rename to src/Discord.Net/API/Rest/ModifyGuildMember.cs index ce1649bdd..07a662645 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMember.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildMember.cs @@ -1,7 +1,7 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class UpdateMemberRequest : IRestRequest @@ -10,17 +10,17 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; object IRestRequest.Payload => this; - public ulong GuildId { get; set; } - public ulong UserId { get; set; } + public ulong GuildId { get; } + public ulong UserId { get; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } [JsonProperty("mute")] - public bool IsMuted { get; set; } + public bool Mute { get; set; } [JsonProperty("deaf")] - public bool IsDeafened { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? VoiceChannelId { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] RoleIds { get; set; } + public bool Deaf { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } public UpdateMemberRequest(ulong guildId, ulong userId) { diff --git a/src/Discord.Net/API/Rest/ModifyGuildRole.cs b/src/Discord.Net/API/Rest/ModifyGuildRole.cs new file mode 100644 index 000000000..6d7e09c59 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildRole.cs @@ -0,0 +1,32 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class ModifyGuildRoleRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + public ulong RoleId { get; } + + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("permissions")] + public int Permissions { get; set; } + [JsonProperty("position")] + public int Position { get; set; } + [JsonProperty("color")] + public int Color { get; set; } + [JsonProperty("hoist")] + public bool Hoist { get; set; } + + public ModifyGuildRoleRequest(ulong guildId, ulong roleId) + { + GuildId = guildId; + RoleId = roleId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildRoles.cs b/src/Discord.Net/API/Rest/ModifyGuildRoles.cs new file mode 100644 index 000000000..905f51b11 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildRoles.cs @@ -0,0 +1,21 @@ +using Discord.Net.Rest; +using System; + +namespace Discord.API.Rest +{ + public class ModifyGuildRolesRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; + object IRestRequest.Payload => Requests; + + public ulong GuildId { get; } + + public ModifyGuildRoleRequest[] Requests { get; set; } = Array.Empty(); + + public ModifyGuildRolesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs b/src/Discord.Net/API/Rest/ModifyMessage.cs similarity index 77% rename from src/Discord.Net/API/Client/Rest/UpdateMessage.cs rename to src/Discord.Net/API/Rest/ModifyMessage.cs index fc055b2bc..0c99403a6 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs +++ b/src/Discord.Net/API/Rest/ModifyMessage.cs @@ -1,6 +1,7 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class UpdateMessageRequest : IRestRequest @@ -9,8 +10,8 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } + public ulong ChannelId { get; } + public ulong MessageId { get; } [JsonProperty("content")] public string Content { get; set; } = ""; diff --git a/src/Discord.Net/API/Rest/ModifyTextChannel.cs b/src/Discord.Net/API/Rest/ModifyTextChannel.cs new file mode 100644 index 000000000..f3d672daf --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyTextChannel.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyTextChannelRequest : ModifyGuildChannelRequest + { + [JsonProperty("topic")] + public string Topic { get; set; } + + public ModifyTextChannelRequest(ulong channelId) + : base(channelId) + { + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs b/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs new file mode 100644 index 000000000..e8bdfac8f --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyVoiceChannelRequest : ModifyGuildChannelRequest + { + [JsonProperty("bitrate")] + public int Bitrate { get; set; } + + public ModifyVoiceChannelRequest(ulong channelId) + : base(channelId) + { + } + } +} diff --git a/src/Discord.Net/API/Rest/QueryUser.cs b/src/Discord.Net/API/Rest/QueryUser.cs new file mode 100644 index 000000000..45a194805 --- /dev/null +++ b/src/Discord.Net/API/Rest/QueryUser.cs @@ -0,0 +1,19 @@ +using Discord.Net.Rest; +using System; + +namespace Discord.API.Rest +{ + public class QueryUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users?q={Uri.EscapeDataString(Query)}&limit={Limit}"; + object IRestRequest.Payload => null; + + public string Query { get; set; } + public int Limit { get; set; } = 25; + + public QueryUserRequest() + { + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs b/src/Discord.Net/API/Rest/RemoveGuildBan.cs similarity index 67% rename from src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs rename to src/Discord.Net/API/Rest/RemoveGuildBan.cs index 5a8f4f796..4f5df1243 100644 --- a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs +++ b/src/Discord.Net/API/Rest/RemoveGuildBan.cs @@ -1,16 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class RemoveGuildBanRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } - public ulong UserId { get; set; } + public ulong GuildId { get; } + public ulong UserId { get; } public RemoveGuildBanRequest(ulong guildId, ulong userId) { diff --git a/src/Discord.Net/API/Rest/RemoveGuildMember.cs b/src/Discord.Net/API/Rest/RemoveGuildMember.cs new file mode 100644 index 000000000..a5b39b1db --- /dev/null +++ b/src/Discord.Net/API/Rest/RemoveGuildMember.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class RemoveGuildMemberRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + public RemoveGuildMemberRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Rest/SyncGuildIntegration.cs b/src/Discord.Net/API/Rest/SyncGuildIntegration.cs new file mode 100644 index 000000000..4c7f0acfa --- /dev/null +++ b/src/Discord.Net/API/Rest/SyncGuildIntegration.cs @@ -0,0 +1,21 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class SyncGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}/sync"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + public SyncGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs b/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs new file mode 100644 index 000000000..3c0baa855 --- /dev/null +++ b/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class TriggerTypingIndicatorRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public TriggerTypingIndicatorRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/SendFile.cs b/src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/SendFile.cs rename to src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs index 4b59e1a11..d7252e7d4 100644 --- a/src/Discord.Net/API/Client/Rest/SendFile.cs +++ b/src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs @@ -1,21 +1,31 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class SendFileRequest : IRestFileRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; object IRestRequest.Payload => null; + string IRestFileRequest.Filename => Filename; Stream IRestFileRequest.Stream => Stream; + IReadOnlyList IRestFileRequest.MultipartParameters => ImmutableArray.Create( + new RestParameter("content", Content), + new RestParameter("nonce", Nonce), + new RestParameter("tts", IsTTS) + ); - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } public string Filename { get; set; } public Stream Stream { get; set; } + public string Content { get; set; } + public string Nonce { get; set; } + public bool IsTTS { get; set; } public SendFileRequest(ulong channelId) { diff --git a/src/Discord.Net/API/Status/Common/StatusResult.cs b/src/Discord.Net/API/Status/Common/StatusResult.cs deleted file mode 100644 index 74728c578..000000000 --- a/src/Discord.Net/API/Status/Common/StatusResult.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Status -{ - public class StatusResult - { - public class PageData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("updated_at")] - public DateTime? UpdatedAt { get; set; } - } - - public class IncidentData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("page_id")] - public string PageId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("shortlink")] - public string Shortlink { get; set; } - [JsonProperty("impact")] - public string Impact { get; set; } - - [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } - [JsonProperty("updated_at")] - public DateTime UpdatedAt { get; set; } - [JsonProperty("monitoring_at")] - public DateTime? MonitoringAt { get; set; } - [JsonProperty("resolved_at")] - public DateTime? ResolvedAt { get; set; } - [JsonProperty("scheduled_for")] - public DateTime StartTime { get; set; } - [JsonProperty("scheduled_until")] - public DateTime EndTime { get; set; } - - [JsonProperty("incident_updates")] - public IncidentUpdateData[] Updates { get; set; } - } - - public class IncidentUpdateData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("incident_id")] - public string IncidentId { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("body")] - public string Body { get; set; } - - [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } - [JsonProperty("updated_at")] - public DateTime? UpdatedAt { get; set; } - [JsonProperty("display_at")] - public DateTime? DisplayAt { get; set; } - - } - - [JsonProperty("page")] - public PageData Page { get; set; } - [JsonProperty("scheduled_maintenances")] - public IncidentData[] ScheduledMaintenances { get; set; } - [JsonProperty("incidents")] - public IncidentData[] Incidents { get; set; } - } -} diff --git a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs b/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs deleted file mode 100644 index 639f85f08..000000000 --- a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetActiveMaintenancesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"scheduled-maintenances/active.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/AllIncidents.cs b/src/Discord.Net/API/Status/Rest/AllIncidents.cs deleted file mode 100644 index 9575bbd43..000000000 --- a/src/Discord.Net/API/Status/Rest/AllIncidents.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetAllIncidentsRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"incidents.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs b/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs deleted file mode 100644 index 3cff11c23..000000000 --- a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetUnresolvedIncidentsRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"incidents/unresolved.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs b/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs deleted file mode 100644 index 803a8a630..000000000 --- a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetUpcomingMaintenancesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"scheduled-maintenances/upcoming.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs b/src/Discord.Net/API/VoiceSocket/OpCode.cs similarity index 81% rename from src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs rename to src/Discord.Net/API/VoiceSocket/OpCode.cs index e82ab5286..d9174be22 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs +++ b/src/Discord.Net/API/VoiceSocket/OpCode.cs @@ -1,10 +1,10 @@ -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { - public enum OpCodes : byte + public enum OpCode : byte { /// C→S - Used to associate a connection with a token. Identify = 0, - /// C→S - Used to specify configuration. + /// C→S - Used to specify protocol configuration. SelectProtocol = 1, /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. Ready = 2, diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs new file mode 100644 index 000000000..f2f356854 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.API.VoiceSocket +{ + public class HeartbeatCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Heartbeat; + object IWebSocketMessage.Payload => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } +} diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs new file mode 100644 index 000000000..06a63e3c2 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.VoiceSocket +{ + public class IdentifyCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Identify; + object IWebSocketMessage.Payload => this; + + [JsonProperty("server_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs new file mode 100644 index 000000000..4aa71b4f3 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Discord.API.VoiceSocket +{ + public class SelectProtocolCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.SelectProtocol; + object IWebSocketMessage.Payload => this; + + public class ProtocolData + { + [JsonProperty("address")] + public string Address { get; set; } + [JsonProperty("port")] + public int Port { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } + [JsonProperty("protocol")] + public string Protocol { get; set; } = "udp"; + [JsonProperty("data")] + private ProtocolData Data { get; } = new ProtocolData(); + + public string ExternalAddress { get { return Data.Address; } set { Data.Address = value; } } + public int ExternalPort { get { return Data.Port; } set { Data.Port = value; } } + public string EncryptionMode { get { return Data.Mode; } set { Data.Mode = value; } } + } +} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs similarity index 66% rename from src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs index 6022c4d58..0476b9a7d 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SetSpeakingCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Speaking; + int IWebSocketMessage.OpCode => (int)OpCode.Speaking; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("speaking")] public bool IsSpeaking { get; set; } diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs similarity index 90% rename from src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs index 6fdced897..7ba700d96 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class ReadyEvent { diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs similarity index 85% rename from src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs index 042c5278d..09bda01c1 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SessionDescriptionEvent { diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs similarity index 57% rename from src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs index 59268c4e6..0e1271f98 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SpeakingEvent { - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("user_id")] public ulong UserId { get; set; } [JsonProperty("ssrc")] public uint SSRC { get; set; } diff --git a/src/Discord.Net/CDN.cs b/src/Discord.Net/CDN.cs new file mode 100644 index 000000000..11786436b --- /dev/null +++ b/src/Discord.Net/CDN.cs @@ -0,0 +1,12 @@ +namespace Discord +{ + internal static class CDN + { + public static string GetUserAvatarUrl(ulong userId, string avatarId) + => avatarId != null ? $"{DiscordConfig.ClientAPIUrl}users/{userId}/avatars/{avatarId}.jpg" : null; + public static string GetGuildIconUrl(ulong guildId, string iconId) + => iconId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/icons/{iconId}.jpg" : null; + public static string GetGuildSplashUrl(ulong guildId, string splashId) + => splashId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/splashes/{splashId}.jpg" : null; + } +} diff --git a/src/Discord.Net/Discord.Net.Net45.csproj b/src/Discord.Net/Discord.Net.Net45.csproj new file mode 100644 index 000000000..f5d12fb3f --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.csproj @@ -0,0 +1,305 @@ + + + + + + Debug + AnyCPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608} + Library + Properties + Discord + Discord.Net + v4.6.1 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + MinimumRecommendedRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.Net45.project.json b/src/Discord.Net/Discord.Net.Net45.project.json new file mode 100644 index 000000000..0d36751ed --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.project.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "Newtonsoft.Json": "8.0.3", + "System.Collections.Immutable": "1.2.0-rc2-23608" + }, + "frameworks": { + "net461": {} + }, + "runtimes": { + "win": {} + } +} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.Net45.project.lock.json b/src/Discord.Net/Discord.Net.Net45.project.lock.json new file mode 100644 index 000000000..aad5884f0 --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.project.lock.json @@ -0,0 +1,88 @@ +{ + "locked": false, + "version": 2, + "targets": { + ".NETFramework,Version=v4.6.1": { + "Newtonsoft.Json/8.0.3": { + "type": "package", + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "type": "package", + "compile": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + } + } + }, + ".NETFramework,Version=v4.6.1/win": { + "Newtonsoft.Json/8.0.3": { + "type": "package", + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "type": "package", + "compile": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + } + } + } + }, + "libraries": { + "Newtonsoft.Json/8.0.3": { + "sha512": "KGsYQdS2zLH+H8x2cZaSI7e+YZ4SFIbyy1YJQYl6GYBWjf5o4H1A68nxyq+WTyVSOJQ4GqS/DiPE+UseUizgMg==", + "type": "package", + "files": [ + "Newtonsoft.Json.8.0.3.nupkg.sha512", + "Newtonsoft.Json.nuspec", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml", + "lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.dll", + "lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.xml", + "tools/install.ps1" + ] + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "sha512": "LIodNcjmeDMzZ0P0nadxBAiZcxwTNXmiHMJoyj1xO2vvahd617xLnO8tJrWNCKgPcwDimuAC9twqsQRFiDOuDQ==", + "type": "package", + "files": [ + "System.Collections.Immutable.1.2.0-rc2-23608.nupkg.sha512", + "System.Collections.Immutable.nuspec", + "lib/dotnet5.1/System.Collections.Immutable.dll", + "lib/dotnet5.1/System.Collections.Immutable.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.xml" + ] + } + }, + "projectFileDependencyGroups": { + "": [ + "Newtonsoft.Json >= 8.0.3", + "System.Collections.Immutable >= 1.2.0-rc2-23608" + ], + ".NETFramework,Version=v4.6.1": [] + } +} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj index be1dbc400..e5ec681b5 100644 --- a/src/Discord.Net/Discord.Net.xproj +++ b/src/Discord.Net/Discord.Net.xproj @@ -1,20 +1,20 @@  - + - 14.0 + 14.0.25123 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - acfb060b-ec8a-4926-b293-04c01e17ee23 + 2c91bdd7-621d-460f-b768-ead106d9ba62 Discord - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ + ..\..\..\TestBot\artifacts\obj\$(MSBuildProjectName) + ..\..\..\TestBot\artifacts\bin\$(MSBuildProjectName)\ 2.0 - + True diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs deleted file mode 100644 index b05725f9b..000000000 --- a/src/Discord.Net/DiscordClient.Events.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Discord -{ - public partial class DiscordClient - { - public event EventHandler Ready = delegate { }; - //public event EventHandler LoggedOut = delegate { }; - public event EventHandler ChannelCreated = delegate { }; - public event EventHandler ChannelDestroyed = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; - public event EventHandler MessageAcknowledged = delegate { }; - public event EventHandler MessageDeleted = delegate { }; - public event EventHandler MessageReceived = delegate { }; - public event EventHandler MessageSent = delegate { }; - public event EventHandler MessageUpdated = delegate { }; - public event EventHandler ProfileUpdated = delegate { }; - public event EventHandler RoleCreated = delegate { }; - public event EventHandler RoleUpdated = delegate { }; - public event EventHandler RoleDeleted = delegate { }; - public event EventHandler JoinedServer = delegate { }; - public event EventHandler LeftServer = delegate { }; - public event EventHandler ServerAvailable = delegate { }; - public event EventHandler ServerUpdated = delegate { }; - public event EventHandler ServerUnavailable = delegate { }; - public event EventHandler UserBanned = delegate { }; - public event EventHandler UserIsTyping = delegate { }; - public event EventHandler UserJoined = delegate { }; - public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserUnbanned = delegate { }; - - private void OnReady() - => OnEvent(Ready); - /*private void OnLoggedOut(bool wasUnexpected, Exception ex) - => OnEvent(LoggedOut, new DisconnectedEventArgs(wasUnexpected, ex));*/ - - private void OnChannelCreated(IChannel channel) - => OnEvent(ChannelCreated, new ChannelEventArgs(channel)); - private void OnChannelDestroyed(IChannel channel) - => OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); - private void OnChannelUpdated(IChannel before, IChannel after) - => OnEvent(ChannelUpdated, new ChannelUpdatedEventArgs(before, after)); - - private void OnMessageAcknowledged(Message msg) - => OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); - private void OnMessageDeleted(Message msg) - => OnEvent(MessageDeleted, new MessageEventArgs(msg)); - private void OnMessageReceived(Message msg) - => OnEvent(MessageReceived, new MessageEventArgs(msg)); - internal void OnMessageSent(Message msg) - => OnEvent(MessageSent, new MessageEventArgs(msg)); - private void OnMessageUpdated(Message before, Message after) - => OnEvent(MessageUpdated, new MessageUpdatedEventArgs(before, after)); - - private void OnProfileUpdated(Profile before, Profile after) - => OnEvent(ProfileUpdated, new ProfileUpdatedEventArgs(before, after)); - - private void OnRoleCreated(Role role) - => OnEvent(RoleCreated, new RoleEventArgs(role)); - private void OnRoleDeleted(Role role) - => OnEvent(RoleDeleted, new RoleEventArgs(role)); - private void OnRoleUpdated(Role before, Role after) - => OnEvent(RoleUpdated, new RoleUpdatedEventArgs(before, after)); - - private void OnJoinedServer(Server server) - => OnEvent(JoinedServer, new ServerEventArgs(server)); - private void OnLeftServer(Server server) - => OnEvent(LeftServer, new ServerEventArgs(server)); - private void OnServerAvailable(Server server) - => OnEvent(ServerAvailable, new ServerEventArgs(server)); - private void OnServerUpdated(Server before, Server after) - => OnEvent(ServerUpdated, new ServerUpdatedEventArgs(before, after)); - private void OnServerUnavailable(Server server) - => OnEvent(ServerUnavailable, new ServerEventArgs(server)); - - private void OnUserBanned(User user) - => OnEvent(UserBanned, new UserEventArgs(user)); - private void OnUserIsTypingUpdated(ITextChannel channel, User user) - => OnEvent(UserIsTyping, new TypingEventArgs(channel, user)); - private void OnUserJoined(User user) - => OnEvent(UserJoined, new UserEventArgs(user)); - private void OnUserLeft(User user) - => OnEvent(UserLeft, new UserEventArgs(user)); - private void OnUserUnbanned(User user) - => OnEvent(UserUnbanned, new UserEventArgs(user)); - private void OnUserUpdated(User before, User after) - => OnEvent(UserUpdated, new UserUpdatedEventArgs(before, after)); - - private void OnEvent(EventHandler handler, T eventArgs, [CallerMemberName] string callerName = null) - { - try { handler(this, eventArgs); } - catch (Exception ex) - { - Logger.Error($"{callerName.Substring(2)}'s handler encountered an error", ex); - } - } - private void OnEvent(EventHandler handler, [CallerMemberName] string callerName = null) - { - try { handler(this, EventArgs.Empty); } - catch (Exception ex) - { - Logger.Error($"{callerName.Substring(2)}'s handler encountered an error", ex); - } - } - } -} diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 5fa94bbc2..d59f7fec1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,398 +1,136 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.GatewaySocket; -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Logging; using Discord.Net; using Discord.Net.Rest; -using Discord.Net.WebSockets; -using Newtonsoft.Json; -using Nito.AsyncEx; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Discord { - /// Provides a connection to the DiscordApp service. - public partial class DiscordClient : IDisposable + public class DiscordClient : IDisposable { - private readonly AsyncLock _connectionLock; - private readonly ManualResetEvent _disconnectedEvent; - private readonly ManualResetEventSlim _connectedEvent; - private readonly TaskManager _taskManager; - private readonly ServiceCollection _services; - private ConcurrentDictionary _servers; - private ConcurrentDictionary _channels; - private ConcurrentDictionary _privateChannels; //Key = RecipientId - private Dictionary _regions; - private Stopwatch _connectionStopwatch; - - internal Logger Logger { get; } - - /// Gets the configuration object used to make this client. - public DiscordConfig Config { get; } - /// Gets the log manager. - public LogManager Log { get; } - /// Gets the internal RestClient for the Client API endpoint. - public RestClient ClientAPI { get; } - /// Gets the internal RestClient for the Status API endpoint. - public RestClient StatusAPI { get; } - /// Gets the internal WebSocket for the Gateway event stream. - public GatewaySocket GatewaySocket { get; } + public event EventHandler Log; + public event EventHandler LoggedIn, LoggedOut; + + protected readonly RestClientProvider _restClientProvider; + protected readonly string _token; + protected readonly LogManager _logManager; + protected readonly SemaphoreSlim _connectionLock; + protected readonly Logger _restLogger; + protected CancellationTokenSource _cancelToken; + protected bool _isDisposed; + + public string UserAgent { get; } + public IReadOnlyList VoiceRegions { get; private set; } + /// Gets the internal RestClient. + public RestClient RestClient { get; protected set; } /// Gets the queue used for outgoing messages, if enabled. - public MessageQueue MessageQueue { get; } - /// Gets the JSON serializer used by this client. - public JsonSerializer Serializer { get; } - - /// Gets the current connection state of this client. - public ConnectionState State { get; private set; } - /// Gets a cancellation token that triggers when the client is manually disconnected. - public CancellationToken CancelToken { get; private set; } - /// Gets the current logged-in user used in private channels. - internal User PrivateUser { get; private set; } - /// Gets information about the current logged-in account. - public Profile CurrentUser { get; private set; } - /// Gets the session id for the current connection. - public string SessionId { get; private set; } - /// Gets the status of the current user. - public UserStatus Status { get; private set; } - /// Gets the game the current user is displayed as playing. - public string CurrentGame { get; private set; } + public MessageQueue MessageQueue { get; protected set; } + public SelfUser CurrentUser { get; protected set; } + public bool IsLoggedIn { get; private set; } - /// Gets a collection of all extensions added to this DiscordClient. - public IEnumerable Services => _services; - /// Gets a collection of all servers this client is a member of. - public IEnumerable Servers => _servers.Select(x => x.Value); - /// Gets a collection of all private channels this client is a member of. - public IEnumerable PrivateChannels => _privateChannels.Select(x => x.Value); - /// Gets a collection of all voice regions currently offered by Discord. - public IEnumerable Regions => _regions.Select(x => x.Value); + internal CancellationToken CancelToken => _cancelToken.Token; - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(Action configFunc) - : this(ProcessConfig(configFunc)) - { - } - private static DiscordConfigBuilder ProcessConfig(Action func) - { - var config = new DiscordConfigBuilder(); - func(config); - return config; - } - - /// Initializes a new instance of the DiscordClient class. - public DiscordClient() - : this(new DiscordConfigBuilder()) - { - } - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(DiscordConfigBuilder builder) - : this(builder.Build()) - { - if (builder.LogHandler != null) - Log.Message += builder.LogHandler; - } - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(DiscordConfig config) + public DiscordClient(string token, DiscordConfig config = null) { - Config = config; - - State = (int)ConnectionState.Disconnected; - Status = UserStatus.Online; - - //Logging - Log = new LogManager(this); - Logger = Log.CreateLogger("Discord"); - if (config.LogLevel >= LogSeverity.Verbose) - _connectionStopwatch = new Stopwatch(); + if (token == null) throw new ArgumentNullException(nameof(token)); - //Async - _taskManager = new TaskManager(Cleanup); - _connectionLock = new AsyncLock(); - _disconnectedEvent = new ManualResetEvent(true); - _connectedEvent = new ManualResetEventSlim(false); - CancelToken = new CancellationToken(true); - - //Cache - //ConcurrentLevel = 2 (only REST and WebSocket can add/remove) - _servers = new ConcurrentDictionary(2, 0); - _channels = new ConcurrentDictionary(2, 0); - _privateChannels = new ConcurrentDictionary(2, 0); - - //Serialization - Serializer = new JsonSerializer(); - Serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; -#if TEST_RESPONSES - Serializer.CheckAdditionalContent = true; - Serializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - Serializer.CheckAdditionalContent = false; - Serializer.MissingMemberHandling = MissingMemberHandling.Ignore; -#endif - Serializer.Error += (s, e) => - { - e.ErrorContext.Handled = true; - Logger.Error("Serialization Failed", e.ErrorContext.Error); - }; + if (config == null) + config = new DiscordConfig(); - //Networking - ClientAPI = new JsonRestClient(Config, DiscordConfig.ClientAPIUrl, Log.CreateLogger("ClientAPI")); - StatusAPI = new JsonRestClient(Config, DiscordConfig.StatusAPIUrl, Log.CreateLogger("StatusAPI")); - GatewaySocket = new GatewaySocket(Config, Serializer, Log.CreateLogger("Gateway")); - - //GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); - GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); + _token = token; + _connectionLock = new SemaphoreSlim(1, 1); - MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); - - //Extensibility - _services = new ServiceCollection(this); - } - - /// Connects to the Discord server with the provided email and password. - /// Returns a token that can be optionally stored to speed up future connections. - public async Task Connect(string email, string password, string token = null) - { - if (email == null) throw new ArgumentNullException(email); - if (password == null) throw new ArgumentNullException(password); + _restClientProvider = config.RestClientProvider; + UserAgent = GetUserAgent(config.AppName, config.AppVersion, config.AppUrl); - await BeginConnect(email, password, null).ConfigureAwait(false); - return ClientAPI.Token; - } - /// Connects to the Discord server with the provided token. - public async Task Connect(string token) - { - if (token == null) throw new ArgumentNullException(token); - - await BeginConnect(null, null, token).ConfigureAwait(false); + _logManager = new LogManager(config.LogLevel); + _logManager.Message += (s, e) => Log(this, e); + _restLogger = _logManager.CreateLogger("Rest"); } - private async Task BeginConnect(string email, string password, string token = null) + public async Task Login() { + await _connectionLock.WaitAsync().ConfigureAwait(false); try { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - await Disconnect().ConfigureAwait(false); - _taskManager.ClearException(); - - Stopwatch stopwatch = null; - if (Config.LogLevel >= LogSeverity.Verbose) - { - _connectionStopwatch.Restart(); - stopwatch = Stopwatch.StartNew(); - } - State = ConnectionState.Connecting; - _disconnectedEvent.Reset(); - - var cancelSource = new CancellationTokenSource(); - CancelToken = cancelSource.Token; - ClientAPI.CancelToken = CancelToken; - StatusAPI.CancelToken = CancelToken; - - await Login(email, password, token).ConfigureAwait(false); - await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); - - var tasks = new[] { CancelToken.Wait() } - .Concat(MessageQueue.Run(CancelToken)); - - await _taskManager.Start(tasks, cancelSource).ConfigureAwait(false); - GatewaySocket.WaitForConnection(CancelToken); - - if (Config.LogLevel >= LogSeverity.Verbose) - { - stopwatch.Stop(); - double seconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2); - Logger.Verbose($"Handshake + Ready took {seconds} sec"); - } - } - } - catch (Exception ex) - { - await _taskManager.SignalError(ex).ConfigureAwait(false); - throw; + await LoginInternal().ConfigureAwait(false); } + finally { _connectionLock.Release(); } } - private async Task Login(string email = null, string password = null, string token = null) + protected virtual async Task LoginInternal() { - string tokenPath = null, oldToken = null; - byte[] cacheKey = null; + if (IsLoggedIn) + await LogoutInternal().ConfigureAwait(false); - //Get Token - if (email != null && Config.CacheDir != null) + try { - tokenPath = GetTokenCachePath(email); - if (token == null && password != null) - { - Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, - new byte[] { 0x5A, 0x2A, 0xF8, 0xCF, 0x78, 0xD3, 0x7D, 0x0D }); - cacheKey = deriveBytes.GetBytes(16); + _cancelToken = new CancellationTokenSource(); - oldToken = LoadToken(tokenPath, cacheKey); - token = oldToken; - } - } + RestClient = new RestClient(_restClientProvider(DiscordConfig.ClientAPIUrl, _cancelToken.Token)); + RestClient.SetHeader("accept", "*/*"); + RestClient.SetHeader("authorization", _token); + RestClient.SetHeader("user-agent", UserAgent); + RestClient.SentRequest += (s, e) => _restLogger.Verbose($"{e.Request.Method} {e.Request.Endpoint}: {e.Milliseconds} ms"); - ClientAPI.Token = token; - var request = new LoginRequest() { Email = email, Password = password }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); - token = response.Token; - if (Config.CacheDir != null && token != oldToken && tokenPath != null) - SaveToken(tokenPath, cacheKey, token); - ClientAPI.Token = token; + MessageQueue = new MessageQueue(RestClient, _restLogger); + await MessageQueue.Start(_cancelToken.Token).ConfigureAwait(false); - //Cache other stuff - var regionsResponse = (await ClientAPI.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false)); - _regions = regionsResponse.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port, x.Vip)) - .ToDictionary(x => x.Id); - } - private void EndConnect() - { - if (State == ConnectionState.Connecting) - { - State = ConnectionState.Connected; - _connectedEvent.Set(); + var selfResponse = await RestClient.Send(new GetCurrentUserRequest()).ConfigureAwait(false); + var regionsResponse = await RestClient.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); - if (Config.LogLevel >= LogSeverity.Verbose) - { - _connectionStopwatch.Stop(); - double seconds = Math.Round(_connectionStopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2); - Logger.Verbose($"Connection took {seconds} sec"); - } + CurrentUser = CreateSelfUser(selfResponse); + VoiceRegions = regionsResponse.Select(x => CreateVoiceRegion(x)).ToImmutableArray(); - SendStatus(); - OnReady(); + IsLoggedIn = true; + RaiseEvent(LoggedIn); } + catch (Exception) { await LogoutInternal().ConfigureAwait(false); throw; } } - /// Disconnects from the Discord server, canceling any pending requests. - public Task Disconnect() => _taskManager.Stop(true); - private async Task Cleanup() + public async Task Logout() { - var oldState = State; - State = ConnectionState.Disconnecting; - - if (oldState == ConnectionState.Connected) + _cancelToken?.Cancel(); + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } - catch (OperationCanceledException) { } + await LogoutInternal().ConfigureAwait(false); } - - MessageQueue.Clear(); - - await GatewaySocket.Disconnect().ConfigureAwait(false); - ClientAPI.Token = null; - - _servers.Clear(); - _channels.Clear(); - _privateChannels.Clear(); - - PrivateUser = null; - CurrentUser = null; - - State = (int)ConnectionState.Disconnected; - _connectedEvent.Reset(); - _disconnectedEvent.Set(); + finally { _connectionLock.Release(); } } - - public void SetStatus(UserStatus status) + protected virtual async Task LogoutInternal() { - if (status == null) throw new ArgumentNullException(nameof(status)); - if (status != UserStatus.Online && status != UserStatus.Idle) - throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); + bool wasLoggedIn = IsLoggedIn; - Status = status; - SendStatus(); - } - public void SetGame(string game) - { - CurrentGame = game; - SendStatus(); - } - private void SendStatus() - { - PrivateUser.Status = Status; - PrivateUser.CurrentGame = CurrentGame; - foreach (var server in Servers) - { - var current = server.CurrentUser; - if (current != null) - { - current.Status = Status; - current.CurrentGame = CurrentGame; - } - } - var socket = GatewaySocket; - if (socket != null) - socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame); - } + try { _cancelToken.Cancel(); } catch { } + try { await MessageQueue.Stop().ConfigureAwait(false); } catch { } - #region Channels - internal void AddChannel(IChannel channel) - { - _channels.GetOrAdd(channel.Id, channel); - } - private IChannel RemoveChannel(ulong id) - { - IChannel channel; - if (_channels.TryRemove(id, out channel)) + RestClient = null; + MessageQueue = null; + + if (wasLoggedIn) { - if (channel.IsPrivate) - { - PrivateChannel removed; - _privateChannels.TryRemove((channel as PrivateChannel).Recipient.Id, out removed); - } - else - (channel as PublicChannel).Server.RemoveChannel(id); + IsLoggedIn = false; + RaiseEvent(LoggedOut); } - return channel; - } - public IChannel GetChannel(ulong id) - { - IChannel channel; - _channels.TryGetValue(id, out channel); - return channel; } - private PrivateChannel AddPrivateChannel(APIChannel model) - { - IChannel channel; - if (_channels.TryGetOrAdd(model.Id, x => new PrivateChannel(x, new User(model.Recipient, this, null), model), out channel)) - _privateChannels[model.Recipient.Id] = channel as PrivateChannel; - return channel as PrivateChannel; - } - internal PrivateChannel GetPrivateChannel(ulong recipientId) + public virtual async Task> GetDMChannels() { - PrivateChannel channel; - _privateChannels.TryGetValue(recipientId, out channel); - return channel; - } - public Task CreatePrivateChannel(User user) => CreatePrivateChannel(user.Id); - public async Task CreatePrivateChannel(ulong userId) - { - var channel = GetPrivateChannel(userId); - if (channel != null) return channel; - - var request = new CreatePrivateChannelRequest() { RecipientId = userId }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); - - return AddPrivateChannel(response); + var response = await RestClient.Send(new GetCurrentUserDMsRequest()).ConfigureAwait(false); + var result = ImmutableArray.CreateBuilder(response.Length); + for (int i = 0; i < response.Length; i++) + result[i] = CreateDMChannel(response[i]); + return result.ToImmutable(); } - #endregion - - #region Invites - /// Gets more info about the provided invite code. - /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode - /// The invite object if found, null if not. - public async Task GetInvite(string inviteIdOrXkcd) + public virtual async Task GetInvite(string inviteIdOrXkcd) { if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); @@ -406,697 +144,212 @@ namespace Discord try { - var response = await ClientAPI.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); - var invite = new Invite(response, this); - invite.Update(response); - return invite; + var response = await RestClient.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); + return CreatePublicInvite(response); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - #endregion - - #region Regions - public Region GetRegion(string id) + public virtual async Task GetGuild(ulong id) { - Region region; - if (_regions.TryGetValue(id, out region)) - return region; - else - return new Region(id, id, "", 0, false); + var response = await RestClient.Send(new GetGuildRequest(id)).ConfigureAwait(false); + return CreateGuild(response); } - #endregion - - #region Servers - private Server AddServer(ulong id) => _servers.GetOrAdd(id, x => new Server(x, this)); - private Server RemoveServer(ulong id) + public virtual async Task> GetGuilds() { - Server server; - if (_servers.TryRemove(id, out server)) + var response = await RestClient.Send(new GetCurrentUserGuildsRequest()).ConfigureAwait(false); + var result = ImmutableArray.CreateBuilder(response.Length); + for (int i = 0; i < response.Length; i++) + result[i] = CreateGuild(response[i]); + return result.ToImmutable(); + } + public virtual async Task GetUser(ulong id) + { + var response = await RestClient.Send(new GetUserRequest(id)); + var user = CreatePublicUser(response); + return user; + } + public virtual async Task GetUser(string username, ushort discriminator) + { + var response = await RestClient.Send(new QueryUserRequest() { Query = $"{username}#{discriminator}", Limit = 1 }); + if (response.Length > 0) { - foreach (var channel in server.AllChannels) - RemoveChannel(channel.Id); + var user = CreatePublicUser(response[0]); + return user; } - return server; + return null; } - - public Server GetServer(ulong id) + public virtual VoiceRegion GetOptimalVoiceRegion() + { + var regions = VoiceRegions; + for (int i = 0; i < regions.Count; i++) + { + if (regions[i].IsOptimal) + return regions[i]; + } + return null; + } + public virtual VoiceRegion GetVoiceRegion(string id) { - Server server; - _servers.TryGetValue(id, out server); - return server; + if (id == null) throw new ArgumentNullException(nameof(id)); + + var regions = VoiceRegions; + for (int i = 0; i < regions.Count; i++) + { + if (regions[i].Id == id) + return regions[i]; + } + return null; } - /// Creates a new server with the provided name and region. - public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) + public virtual async Task GetOrCreateDMChannel(ulong userId) + { + var response = await RestClient.Send(new CreateDMChannelRequest + { + RecipientId = userId + }).ConfigureAwait(false); + + return CreateDMChannel(response); + } + /// Creates a new guild with the provided name and region. This function requires your bot to be whitelisted by Discord. + public virtual async Task CreateGuild(string name, VoiceRegion region, Stream jpegIcon = null) { if (name == null) throw new ArgumentNullException(nameof(name)); if (region == null) throw new ArgumentNullException(nameof(region)); - var request = new CreateGuildRequest() + var response = await RestClient.Send(new CreateGuildRequest { Name = name, Region = region.Id, - IconBase64 = icon.Base64(iconType, null) - }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); + Icon = jpegIcon + }).ConfigureAwait(false); - var server = AddServer(response.Id); - server.Update(response); - return server; + return CreateGuild(response); } - #endregion - #region Gateway Events - private void OnReceivedEvent(WebSocketEventEventArgs e) + internal virtual DMChannel CreateDMChannel(API.Channel model) { - try - { - switch (e.Type) - { - //Global - case "READY": - { - //TODO: None of this is really threadsafe - should only replace the cache collections when they have been fully populated - - var data = e.Payload.ToObject(Serializer); - - int channelCount = 0; - for (int i = 0; i < data.Guilds.Length; i++) - channelCount += data.Guilds[i].Channels.Length; - - //ConcurrencyLevel = 2 (only REST and WebSocket can add/remove) - _servers = new ConcurrentDictionary(2, (int)(data.Guilds.Length * 1.05)); - _channels = new ConcurrentDictionary(2, (int)(channelCount * 1.05)); - _privateChannels = new ConcurrentDictionary(2, (int)(data.PrivateChannels.Length * 1.05)); - List largeServers = new List(); - - SessionId = data.SessionId; - PrivateUser = new User(data.User, this, null); - PrivateUser.Update(data.User); - CurrentUser = new Profile(data.User, this); - CurrentUser.Update(data.User); - - for (int i = 0; i < data.Guilds.Length; i++) - { - var model = data.Guilds[i]; - if (model.Unavailable != true) - { - var server = AddServer(model.Id); - server.Update(model); - if (model.IsLarge) - largeServers.Add(server.Id); - } - } - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i]); - if (largeServers.Count > 0) - GatewaySocket.SendRequestMembers(largeServers, "", 0); - else - EndConnect(); - } - break; - - //Servers - case "GUILD_CREATE": - { - var data = e.Payload.ToObject(Serializer); - if (data.Unavailable != true) - { - var server = AddServer(data.Id); - server.Update(data); - - if (data.Unavailable != false) - Logger.Info($"GUILD_CREATE: {server}"); - else - Logger.Info($"GUILD_AVAILABLE: {server}"); - - if (data.Unavailable != false) - OnJoinedServer(server); - OnServerAvailable(server); - } - } - break; - case "GUILD_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.Id); - if (server != null) - { - var before = Config.EnablePreUpdateEvents ? server.Clone() : null; - server.Update(data); - Logger.Info($"GUILD_UPDATE: {server}"); - OnServerUpdated(before, server); - } - else - Logger.Warning("GUILD_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_DELETE": - { - var data = e.Payload.ToObject(Serializer); - Server server = RemoveServer(data.Id); - if (server != null) - { - if (data.Unavailable != true) - Logger.Info($"GUILD_DELETE: {server}"); - else - Logger.Info($"GUILD_UNAVAILABLE: {server}"); - - OnServerUnavailable(server); - if (data.Unavailable != true) - OnLeftServer(server); - } - else - Logger.Warning("GUILD_DELETE referenced an unknown guild."); - } - break; - - //Channels - case "CHANNEL_CREATE": - { - var data = e.Payload.ToObject(Serializer); - - IChannel channel = null; - if (data.GuildId != null) - { - var server = GetServer(data.GuildId.Value); - if (server != null) - channel = server.AddChannel(data, true); - else - { - Logger.Warning("CHANNEL_CREATE referenced an unknown guild."); - break; - } - } - else - channel = AddPrivateChannel(data); - Logger.Info($"CHANNEL_CREATE: {channel}"); - OnChannelCreated(channel); - } - break; - case "CHANNEL_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.Id); - if (channel != null) - { - var before = Config.EnablePreUpdateEvents ? (channel as Channel).Clone() : null; - (channel as Channel).Update(data); - Logger.Info($"CHANNEL_UPDATE: {channel}"); - OnChannelUpdated(before, channel); - } - else - Logger.Warning("CHANNEL_UPDATE referenced an unknown channel."); - } - break; - case "CHANNEL_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var channel = RemoveChannel(data.Id); - if (channel != null) - { - Logger.Info($"CHANNEL_DELETE: {channel}"); - OnChannelDestroyed(channel); - } - else - Logger.Warning("CHANNEL_DELETE referenced an unknown channel."); - } - break; - - //Members - case "GUILD_MEMBER_ADD": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.AddUser(data, true, true); - user.Update(data); - user.UpdateActivity(); - Logger.Info($"GUILD_MEMBER_ADD: {user}"); - OnUserJoined(user); - } - else - Logger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) - { - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - Logger.Info($"GUILD_MEMBER_UPDATE: {user}"); - OnUserUpdated(before, user); - } - else - Logger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); - } - else - Logger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_REMOVE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.RemoveUser(data.User.Id); - if (user != null) - { - Logger.Info($"GUILD_MEMBER_REMOVE: {user}"); - OnUserLeft(user); - } - else - Logger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); - } - else - Logger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); - } - break; - case "GUILD_MEMBERS_CHUNK": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - foreach (var memberData in data.Members) - { - var user = server.AddUser(memberData, true, false); - user.Update(memberData); - } - Logger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); - - if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there - { - bool isConnectComplete = true; - foreach (var server2 in _servers.Select(x => x.Value)) - { - if (server2.CurrentUserCount < server2.UserCount) - isConnectComplete = false; - } - if (isConnectComplete) - EndConnect(); - } - } - else - Logger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); - } - break; - - //Roles - case "GUILD_ROLE_CREATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.AddRole(data.Data.Id); - role.Update(data.Data, false); - Logger.Info($"GUILD_ROLE_CREATE: {role}"); - OnRoleCreated(role); - } - else - Logger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.GetRole(data.Data.Id); - if (role != null) - { - var before = Config.EnablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Data, true); - Logger.Info($"GUILD_ROLE_UPDATE: {role}"); - OnRoleUpdated(before, role); - } - else - Logger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); - } - else - Logger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.RemoveRole(data.RoleId); - if (role != null) - { - Logger.Info($"GUILD_ROLE_DELETE: {role}"); - OnRoleDeleted(role); - } - else - Logger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); - } - else - Logger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); - } - break; - - //Bans - case "GUILD_BAN_ADD": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) - { - Logger.Info($"GUILD_BAN_ADD: {user}"); - OnUserBanned(user); - } - else - Logger.Warning("GUILD_BAN_ADD referenced an unknown user."); - } - else - Logger.Warning("GUILD_BAN_ADD referenced an unknown guild."); - } - break; - case "GUILD_BAN_REMOVE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = new User(data.User, this, server); - user.Update(data.User); - Logger.Info($"GUILD_BAN_REMOVE: {user}"); - OnUserUnbanned(user); - } - else - Logger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); - } - break; - - //Messages - case "MESSAGE_CREATE": - { - var data = e.Payload.ToObject(Serializer); - - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var user = (channel as Channel).GetUser(data.Author.Id); - - if (user != null) - { - Message msg = null; - msg = (channel as Channel).MessageManager.Add(data, user); - user.UpdateActivity(); - - Logger.Verbose($"MESSAGE_CREATE: {channel} ({user})"); - OnMessageReceived(msg); - } - else - Logger.Warning("MESSAGE_CREATE referenced an unknown user."); - } - else - Logger.Warning("MESSAGE_CREATE referenced an unknown channel."); - } - break; - case "MESSAGE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var msg = (channel as Channel).MessageManager.Get(data.Id, data.Author?.Id); - var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data); - Logger.Verbose($"MESSAGE_UPDATE: {channel} ({data.Author?.Username ?? "Unknown"})"); - OnMessageUpdated(before, msg); - } - else - Logger.Warning("MESSAGE_UPDATE referenced an unknown channel."); - } - break; - case "MESSAGE_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var msg = (channel as Channel).MessageManager.Remove(data.Id); - Logger.Verbose($"MESSAGE_DELETE: {channel} ({msg.User?.Name ?? "Unknown"})"); - OnMessageDeleted(msg); - } - else - Logger.Warning("MESSAGE_DELETE referenced an unknown channel."); - } - break; - - //Statuses - case "PRESENCE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - User user; - Server server; - if (data.GuildId == null) - { - server = null; - user = GetPrivateChannel(data.User.Id)?.Recipient; - } - else - { - server = GetServer(data.GuildId.Value); - if (server == null) - { - Logger.Warning("PRESENCE_UPDATE referenced an unknown server."); - break; - } - else - user = server.GetUser(data.User.Id); - } - - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"PRESENCE_UPDATE: {user}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - OnUserUpdated(before, user); - } - /*else //Occurs when a user leaves a server - Logger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ - } - break; - case "TYPING_START": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - User user = (channel as Channel).GetUser(data.UserId); - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"TYPING_START: {user.ToString(channel)}"); - OnUserIsTypingUpdated(channel, user); - user.UpdateActivity(); - } - } - } - break; - - //Voice - case "VOICE_STATE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var user = server.GetUser(data.UserId); - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"VOICE_STATE_UPDATE: {user}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - //Logger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); - OnUserUpdated(before, user); - } - /*else //Occurs when a user leaves a server - Logger.Warning("VOICE_STATE_UPDATE referenced an unknown user.");*/ - } - else - Logger.Warning("VOICE_STATE_UPDATE referenced an unknown server."); - } - break; - - //Settings - case "USER_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - if (data.Id == CurrentUser.Id) - { - var before = Config.EnablePreUpdateEvents ? CurrentUser.Clone() : null; - CurrentUser.Update(data); - PrivateUser.Update(data); - foreach (var server in _servers) - server.Value.CurrentUser.Update(data); - Logger.Info($"USER_UPDATE"); - OnProfileUpdated(before, CurrentUser); - } - } - break; - - //Handled in GatewaySocket - case "RESUMED": - break; - - //Ignored - case "USER_SETTINGS_UPDATE": - case "GUILD_INTEGRATIONS_UPDATE": - case "VOICE_SERVER_UPDATE": - case "GUILD_EMOJIS_UPDATE": - case "MESSAGE_ACK": - Logger.Debug($"{e.Type} [Ignored]"); - break; - - //Others - default: - Logger.Warning($"Unknown message type: {e.Type}"); - break; - } - } - catch (Exception ex) - { - Logger.Error($"Error handling {e.Type} event", ex); - } + var channel = new DMChannel(model.Id, this, 0); + channel.Update(model); + return channel; } - #endregion - - #region Services - public T AddService(T instance) - where T : class, IService - => _services.Add(instance); - public T AddService() - where T : class, IService, new() - => _services.Add(new T()); - public T GetService(bool isRequired = true) - where T : class, IService - => _services.Get(isRequired); - #endregion - - #region Async Wrapper - /// Blocking call that will execute the provided async method and wait until the client has been manually stopped. This is mainly intended for use in console applications. - public void ExecuteAndWait(Func asyncAction) + internal virtual TextChannel CreateTextChannel(Guild guild, API.Channel model) { - asyncAction().GetAwaiter().GetResult(); - _disconnectedEvent.WaitOne(); + var channel = new TextChannel(model.Id, guild, 0, false); + channel.Update(model); + return channel; } - /// Blocking call and wait until the client has been manually stopped. This is mainly intended for use in console applications. - public void Wait() + internal virtual VoiceChannel CreateVoiceChannel(Guild guild, API.Channel model) { - _disconnectedEvent.WaitOne(); + var channel = new VoiceChannel(model.Id, guild, false); + channel.Update(model); + return channel; } - #endregion - - #region IDisposable - private bool _isDisposed = false; - - protected virtual void Dispose(bool isDisposing) + internal virtual GuildInvite CreateGuildInvite(GuildChannel channel, API.InviteMetadata model) { - if (!_isDisposed) - { - if (isDisposing) - { - _disconnectedEvent.Dispose(); - _connectedEvent.Dispose(); - } - _isDisposed = true; - } + var invite = new GuildInvite(model.Code, channel); + invite.Update(model); + return invite; } - - public void Dispose() + internal virtual PublicInvite CreatePublicInvite(API.Invite model) { - Dispose(true); + var invite = new PublicInvite(model.Code, this); + invite.Update(model); + return invite; } - #endregion - - //Helpers - private string GetTokenCachePath(string email) + internal virtual Guild CreateGuild(API.Guild model) { - using (var md5 = MD5.Create()) - { - byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(email.ToLowerInvariant())); - StringBuilder filenameBuilder = new StringBuilder(); - for (int i = 0; i < data.Length; i++) - filenameBuilder.Append(data[i].ToString("x2")); - return Path.Combine(Config.CacheDir, filenameBuilder.ToString()); - } + var guild = new Guild(model.Id, this); + guild.Update(model); + return guild; } - private string LoadToken(string path, byte[] key) + internal virtual Message CreateMessage(IMessageChannel channel, User user, API.Message model) { - if (File.Exists(path)) - { - try - { - using (var fileStream = File.Open(path, FileMode.Open)) - using (var aes = Aes.Create()) - { - byte[] iv = new byte[aes.BlockSize / 8]; - fileStream.Read(iv, 0, iv.Length); - aes.IV = iv; - aes.Key = key; - using (var cryptoStream = new CryptoStream(fileStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) - { - byte[] tokenBuffer = new byte[64]; - int length = cryptoStream.Read(tokenBuffer, 0, tokenBuffer.Length); - return Encoding.UTF8.GetString(tokenBuffer, 0, length); - } - } - } - catch (Exception ex) - { - Logger.Warning("Failed to load cached token. Wrong/changed password?", ex); - } - } - return null; + var msg = new Message(model.Id, channel, user); + msg.Update(model); + return msg; } - private void SaveToken(string path, byte[] key, string token) + internal virtual Role CreateRole(Guild guild, API.Role model) { - byte[] tokenBytes = Encoding.UTF8.GetBytes(token); - try - { - string parentDir = Path.GetDirectoryName(path); - if (!Directory.Exists(parentDir)) - Directory.CreateDirectory(parentDir); + var role = new Role(model.Id, guild); + role.Update(model); + return role; + } + internal virtual GuildUser CreateBannedUser(Guild guild, API.User model) + { + var user = new GuildUser(model.Id, guild, null, null); + user.Update(model); + return user; + } + internal virtual DMUser CreateDMUser(DMChannel channel, API.User model) + { + var user = new DMUser(model.Id, channel); + user.Update(model); + return user; + } + internal virtual GuildUser CreateGuildUser(Guild guild, GuildPresence presence, VoiceState voiceState, API.GuildMember model) + { + var user = new GuildUser(model.User.Id, guild, presence, voiceState); + user.Update(model); + return user; + } + internal virtual PublicUser CreatePublicUser(API.User model) + { + var user = new PublicUser(model.Id, this); + user.Update(model); + return user; + } + internal virtual SelfUser CreateSelfUser(API.User model) + { + var user = new SelfUser(model.Id, this); + user.Update(model); + return user; + } + internal virtual VoiceRegion CreateVoiceRegion(API.Rest.GetVoiceRegionsResponse model) + { + var region = new VoiceRegion(model.Id, this); + region.Update(model); + return region; + } - using (var fileStream = File.Open(path, FileMode.Create)) - using (var aes = Aes.Create()) + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) { - aes.GenerateIV(); - aes.Key = key; - using (var cryptoStream = new CryptoStream(fileStream, aes.CreateEncryptor(), CryptoStreamMode.Write)) - { - fileStream.Write(aes.IV, 0, aes.IV.Length); - cryptoStream.Write(tokenBytes, 0, tokenBytes.Length); - } + MessageQueue.Dispose(); + RestClient.Dispose(); + _connectionLock.Dispose(); } + _isDisposed = true; } - catch (Exception ex) + } + public void Dispose() => Dispose(true); + + private static string GetUserAgent(string appName, string appVersion, string appUrl) + { + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(appName)) { - Logger.Warning("Failed to cache token", ex); + sb.Append(appName); + if (!string.IsNullOrEmpty(appVersion)) + sb.Append($"/{appVersion}"); + if (!string.IsNullOrEmpty(appUrl)) + sb.Append($" ({appUrl})"); + sb.Append(' '); } + sb.Append($"DiscordBot ({DiscordConfig.LibUrl}, v{DiscordConfig.LibVersion})"); + return sb.ToString(); } + + protected void RaiseEvent(EventHandler eventHandler) + => eventHandler?.Invoke(this, EventArgs.Empty); + protected void RaiseEvent(EventHandler eventHandler, T eventArgs) where T : EventArgs + => eventHandler?.Invoke(this, eventArgs); + protected void RaiseEvent(EventHandler eventHandler, Func eventArgs) where T : EventArgs + => eventHandler?.Invoke(this, eventArgs()); } -} \ No newline at end of file +} diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index ab6f92eba..b23bc3204 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -1,15 +1,27 @@ -using System; -using System.IO; +using Discord.Net.Rest; +using Discord.Net.WebSockets; using System.Reflection; -using System.Text; namespace Discord { - public class DiscordConfigBuilder + public class DiscordConfig { - //Global + public const int MaxMessageSize = 2000; + public const int MaxMessagesPerBatch = 100; + + public const string LibName = "Discord.Net"; + public static string LibVersion { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; + public const string LibUrl = "https://github.com/RogueException/Discord.Net"; + + public const string ClientAPIUrl = "https://discordapp.com/api/"; + public const string CDNUrl = "https://cdn.discordapp.com/"; + public const string InviteUrl = "https://discord.gg/"; - /// Gets or sets name of your application, used both for the token cache directory and user agent. + internal const int RestTimeout = 10000; + internal const int MessageQueueInterval = 100; + internal const int WebSocketQueueInterval = 100; + + /// Gets or sets name of your application, used in the user agent. public string AppName { get; set; } = null; /// Gets or sets url to your application, used in the user agent. public string AppUrl { get; set; } = null; @@ -18,124 +30,40 @@ namespace Discord /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - - //WebSocket - + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get; set; } = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; //Performance - - /// Gets or sets whether an encrypted login token should be saved to temp dir after successful login. - public bool CacheToken { get; set; } = true; + /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheSize { get; set; } = 100; /// /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.ServerPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. + /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. /// public bool UsePermissionsCache { get; set; } = true; /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. public bool EnablePreUpdateEvents { get; set; } = true; /// - /// Gets or sets the max number of users a server may have for offline users to be included in the READY packet. Max is 250. + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. /// Decreasing this may reduce CPU usage while increasing login time and network usage. /// public int LargeThreshold { get; set; } = 250; - //Events - - /// Gets or sets a handler for all log messages. - public EventHandler LogHandler { get; set; } - - public DiscordConfig Build() => new DiscordConfig(this); - } - - public class DiscordConfig - { - public const int MaxMessageSize = 2000; - internal const int RestTimeout = 10000; - internal const int MessageQueueInterval = 100; - internal const int WebSocketQueueInterval = 100; - - public const string LibName = "Discord.Net"; - public static string LibVersion => typeof(DiscordConfigBuilder).GetTypeInfo().Assembly.GetName().Version.ToString(3); - public const string LibUrl = "https://github.com/RogueException/Discord.Net"; - - public const string ClientAPIUrl = "https://discordapp.com/api/"; - public const string StatusAPIUrl = "https://srhpyqt94yxb.statuspage.io/api/v2/"; //"https://status.discordapp.com/api/v2/"; - public const string CDNUrl = "https://cdn.discordapp.com/"; - public const string InviteUrl = "https://discord.gg/"; + //Engines - public LogSeverity LogLevel { get; } - - public string AppName { get; } - public string AppUrl { get; } - public string AppVersion { get; } - public string UserAgent { get; } - public string CacheDir { get; } - - public int ConnectionTimeout { get; } - public int ReconnectDelay { get; } - public int FailedReconnectDelay { get; } - - public int LargeThreshold { get; } - public int MessageCacheSize { get; } - public bool UsePermissionsCache { get; } - public bool EnablePreUpdateEvents { get; } - - - internal DiscordConfig(DiscordConfigBuilder builder) - { - LogLevel = builder.LogLevel; - - AppName = builder.AppName; - AppUrl = builder.AppUrl; - AppVersion = builder.AppVersion; - UserAgent = GetUserAgent(builder); - CacheDir = GetCacheDir(builder); - - ConnectionTimeout = builder.ConnectionTimeout; - ReconnectDelay = builder.ReconnectDelay; - FailedReconnectDelay = builder.FailedReconnectDelay; - - MessageCacheSize = builder.MessageCacheSize; - UsePermissionsCache = builder.UsePermissionsCache; - EnablePreUpdateEvents = builder.EnablePreUpdateEvents; - } - - private static string GetUserAgent(DiscordConfigBuilder builder) - { - StringBuilder sb = new StringBuilder(); - if (!string.IsNullOrEmpty(builder.AppName)) - { - sb.Append(builder.AppName); - if (!string.IsNullOrEmpty(builder.AppVersion)) - { - sb.Append('/'); - sb.Append(builder.AppVersion); - } - if (!string.IsNullOrEmpty(builder.AppUrl)) - { - sb.Append(" ("); - sb.Append(builder.AppUrl); - sb.Append(')'); - } - sb.Append(' '); - } - sb.Append($"DiscordBot ({LibUrl}, v{LibVersion})"); - return sb.ToString(); - } - private static string GetCacheDir(DiscordConfigBuilder builder) - { - if (builder.CacheToken) - return Path.Combine(Path.GetTempPath(), builder.AppName ?? "Discord.Net"); - else - return null; - } + /// Gets or sets the REST engine to use. Defaults to DefaultRestClientProvider, which is built around .Net's HttpClient class. + public RestClientProvider RestClientProvider { get; set; } = (url, ct) => new DefaultRestEngine(url, ct); + /// + /// Gets or sets the WebSocket engine to use. Defaults to DefaultWebSocketProvider, which uses .Net's WebSocketClient class. + /// WebSockets are only used if DiscordClient.Connect() is called. + /// + public WebSocketProvider WebSocketProvider { get; set; } = null; } } + diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs new file mode 100644 index 000000000..40beb389e --- /dev/null +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -0,0 +1,176 @@ +using Discord.Logging; +using Discord.Net.WebSockets; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + public class DiscordSocketClient : DiscordClient + { + public event EventHandler Connected, Disconnected; + public event EventHandler VoiceConnected, VoiceDisconnected; + + public event EventHandler ChannelCreated, ChannelDestroyed; + public event EventHandler ChannelUpdated; + public event EventHandler MessageReceived, MessageDeleted; + public event EventHandler MessageUpdated; + public event EventHandler RoleCreated, RoleDeleted; + public event EventHandler RoleUpdated; + public event EventHandler JoinedGuild, LeftGuild; + public event EventHandler GuildAvailable, GuildUnavailable; + public event EventHandler GuildUpdated; + public event EventHandler CurrentUserUpdated; + public event EventHandler UserJoined, UserLeft; + public event EventHandler UserBanned, UserUnbanned; + public event EventHandler UserUpdated; + public event EventHandler UserIsTyping; + + private readonly Logger _discordLogger, _gatewayLogger; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + private readonly bool _enablePreUpdateEvents, _usePermissionCache; + private readonly int _largeThreshold, _messageCacheSize; + private ConcurrentDictionary _guilds; + private ConcurrentDictionary _channels; + private ConcurrentDictionary _privateChannels; //Key = RecipientId + + public int ConnectionId { get; } + public int TotalConnections { get; } + /// Gets the internal WebSocket for the Gateway event stream. + public GatewaySocket GatewaySocket { get; private set; } + /*/// Gets the current logged-in account. + public CurrentUser CurrentUser { get; private set; }*/ + + public bool IsConnected => GatewaySocket.State == ConnectionState.Connected; + + public DiscordSocketClient(string token, int connectionId = 0, int totalConnections = 1, DiscordConfig config = null) + : base(token, config) + { + if (totalConnections < 1) throw new ArgumentOutOfRangeException(nameof(totalConnections)); + if (connectionId < 0) throw new ArgumentOutOfRangeException(nameof(connectionId)); + if (connectionId >= totalConnections) throw new ArgumentException($"{nameof(connectionId)} must be less than {nameof(totalConnections)}.", nameof(connectionId)); + + ConnectionId = connectionId; + TotalConnections = totalConnections; + + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; + + _messageCacheSize = config.MessageCacheSize; + _usePermissionCache = config.UsePermissionsCache; + _enablePreUpdateEvents = config.EnablePreUpdateEvents; + _largeThreshold = config.LargeThreshold; + + _discordLogger = _logManager.CreateLogger("Discord"); + _gatewayLogger = _logManager.CreateLogger("Gateway"); + + _guilds = new ConcurrentDictionary(2, 0); + _channels = new ConcurrentDictionary(2, 0); + _privateChannels = new ConcurrentDictionary(2, 0); + } + + protected override async Task LogoutInternal() + { + await DisconnectInternal().ConfigureAwait(false); + + _guilds.Clear(); + _channels.Clear(); + _privateChannels.Clear(); + + CurrentUser = null; + + await base.LogoutInternal(); + } + + public async Task Connect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + protected virtual Task ConnectInternal() + { + throw new NotImplementedException(); + //GatewaySocket = new GatewaySocket(_webSocketProvider(_cancelToken.Token)); + //GatewaySocket.SetHeader("user-agent", _userAgent); + //await GatewaySocket.Connect(_cancelToken.Token).ConfigureAwait(false); + } + + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + protected virtual async Task DisconnectInternal() + { + if (GatewaySocket != null) + { + await GatewaySocket.Disconnect().ConfigureAwait(false); + GatewaySocket = null; + } + } + + public override async Task GetOrCreateDMChannel(ulong userId) + { + DMChannel channel; + if (_privateChannels.TryGetValue(userId, out channel)) + return channel; + + return await base.GetOrCreateDMChannel(userId).ConfigureAwait(false); + } + public override Task> GetDMChannels() + { + return Task.FromResult(_privateChannels.Select(x => x.Value)); + } + public override Task> GetGuilds() + { + return Task.FromResult(_guilds.Select(x => x.Value)); + } + public override Task GetGuild(ulong id) + { + Guild guild; + _guilds.TryGetValue(id, out guild); + return Task.FromResult(guild); + } + + internal override DMChannel CreateDMChannel(API.Channel model) + { + var channel = new DMChannel(model.Id, this, _messageCacheSize); + channel.Update(model); + return channel; + } + internal override TextChannel CreateTextChannel(Guild guild, API.Channel model) + { + var channel = new TextChannel(model.Id, guild, _messageCacheSize, _usePermissionCache); + channel.Update(model); + return channel; + } + internal override VoiceChannel CreateVoiceChannel(Guild guild, API.Channel model) + { + var channel = new VoiceChannel(model.Id, guild, _usePermissionCache); + channel.Update(model); + return channel; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + GatewaySocket.Dispose(); + } + } + } + } +} diff --git a/src/Discord.Net/DynamicIL.cs b/src/Discord.Net/DynamicIL.cs deleted file mode 100644 index bce89424d..000000000 --- a/src/Discord.Net/DynamicIL.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; - -namespace Discord -{ - internal static class DynamicIL - { - public static Action CreateCopyMethod() - { - var method = new DynamicMethod("CopyTo", null, new[] { typeof(T), typeof(T) }, typeof(T), true); - var generator = method.GetILGenerator(); - var typeInfo = typeof(T).GetTypeInfo(); - - typeInfo.ForEachField(f => - { - generator.Emit(OpCodes.Ldarg_1); //Stack: TargetRef - generator.Emit(OpCodes.Ldarg_0); //Stack: TargetRef, SourceRef - generator.Emit(OpCodes.Ldfld, f); //Stack: TargetRef, Value - generator.Emit(OpCodes.Stfld, f); //Stack: - }); - - generator.Emit(OpCodes.Ret); - - return method.CreateDelegate(typeof(Action)) as Action; - } - - public static void ForEachField(this TypeInfo typeInfo, Action func) - { - var baseType = typeInfo.BaseType; - if (baseType != null) - baseType.GetTypeInfo().ForEachField(func); - - foreach (var field in typeInfo.DeclaredFields.Where(x => !x.IsStatic)) - func(field); - } - public static void ForEachProperty(this TypeInfo typeInfo, Action func) - { - var baseType = typeInfo.BaseType; - if (baseType != null) - baseType.GetTypeInfo().ForEachProperty(func); - - foreach (var prop in typeInfo.DeclaredProperties.Where(x => - (!x.CanRead || !x.GetMethod.IsStatic) && (!x.CanWrite || !x.SetMethod.IsStatic))) - func(prop); - } - } -} diff --git a/src/Discord.Net/ETF/ETFReader.cs b/src/Discord.Net/ETF/ETFReader.cs deleted file mode 100644 index 9e1e4e8ef..000000000 --- a/src/Discord.Net/ETF/ETFReader.cs +++ /dev/null @@ -1,491 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; - -namespace Discord.ETF -{ - public class ETFReader : IDisposable - { - private static readonly ConcurrentDictionary _deserializers = new ConcurrentDictionary(); - private static readonly Dictionary _readMethods = GetPrimitiveReadMethods(); - - private readonly Stream _stream; - private readonly byte[] _buffer; - private readonly bool _leaveOpen; - private readonly Encoding _encoding; - - public ETFReader(Stream stream, bool leaveOpen = false) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - - _stream = stream; - _leaveOpen = leaveOpen; - _buffer = new byte[11]; - _encoding = Encoding.UTF8; - } - - public bool ReadBool() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT) - { - _stream.Read(_buffer, 0, 1); - switch (_buffer[0]) //Length - { - case 4: - ReadTrue(); - return true; - case 5: - ReadFalse(); - return false; - } - } - throw new InvalidDataException(); - } - private void ReadTrue() - { - _stream.Read(_buffer, 0, 4); - if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e') - throw new InvalidDataException(); - } - private void ReadFalse() - { - _stream.Read(_buffer, 0, 5); - if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e') - throw new InvalidDataException(); - } - - public int ReadSByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (sbyte)ReadLongInternal(type); - } - public uint ReadByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (byte)ReadLongInternal(type); - } - public int ReadShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (short)ReadLongInternal(type); - } - public uint ReadUShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (ushort)ReadLongInternal(type); - } - public int ReadInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (int)ReadLongInternal(type); - } - public uint ReadUInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (uint)ReadLongInternal(type); - } - public long ReadLong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return ReadLongInternal(type); - } - public ulong ReadULong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (ulong)ReadLongInternal(type); - } - public long ReadLongInternal(ETFType type) - { - switch (type) - { - case ETFType.SMALL_INTEGER_EXT: - _stream.Read(_buffer, 0, 1); - return _buffer[0]; - case ETFType.INTEGER_EXT: - _stream.Read(_buffer, 0, 4); - return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]); - case ETFType.SMALL_BIG_EXT: - _stream.Read(_buffer, 0, 2); - bool isPositive = _buffer[0] == 0; - byte count = _buffer[1]; - - int shiftValue = (count - 1) * 8; - ulong value = 0; - _stream.Read(_buffer, 0, count); - for (int i = 0; i < count; i++, shiftValue -= 8) - value = value + _buffer[i] << shiftValue; - if (!isPositive) - return -(long)value; - else - return (long)value; - } - throw new InvalidDataException(); - } - - public float ReadSingle() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (float)ReadDoubleInternal(type); - } - public double ReadDouble() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return ReadDoubleInternal(type); - } - public double ReadDoubleInternal(ETFType type) - { - throw new NotImplementedException(); - } - - public bool? ReadNullableBool() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT) - { - _stream.Read(_buffer, 0, 1); - switch (_buffer[0]) //Length - { - case 3: - if (ReadNil()) - return null; - break; - case 4: - ReadTrue(); - return true; - case 5: - ReadFalse(); - return false; - } - } - throw new InvalidDataException(); - } - public int? ReadNullableSByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (sbyte)ReadLongInternal(type); - } - public uint? ReadNullableByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (byte)ReadLongInternal(type); - } - public int? ReadNullableShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (short)ReadLongInternal(type); - } - public uint? ReadNullableUShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (ushort)ReadLongInternal(type); - } - public int? ReadNullableInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (int)ReadLongInternal(type); - } - public uint? ReadNullableUInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (uint)ReadLongInternal(type); - } - public long? ReadNullableLong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return ReadLongInternal(type); - } - public ulong? ReadNullableULong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (ulong)ReadLongInternal(type); - } - public float? ReadNullableSingle() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (float)ReadDoubleInternal(type); - } - public double? ReadNullableDouble() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return ReadDoubleInternal(type); - } - - public string ReadString() - { - throw new NotImplementedException(); - } - public byte[] ReadByteArray() - { - throw new NotImplementedException(); - } - - public T Read() - where T : new() - { - var type = typeof(T); - var typeInfo = type.GetTypeInfo(); - var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer(type, typeInfo)) as Func; - return action(this); - } - /*public void Read() - where T : Nullable - where U : struct, new() - { - }*/ - public T[] ReadArray() - { - throw new NotImplementedException(); - } - public IDictionary ReadDictionary() - { - throw new NotImplementedException(); - } - /*public object Read(object obj) - { - throw new NotImplementedException(); - }*/ - - private bool ReadNil(bool ignoreLength = false) - { - if (!ignoreLength) - { - _stream.Read(_buffer, 0, 1); - byte length = _buffer[0]; - if (length != 3) return false; - } - - _stream.Read(_buffer, 0, 3); - if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l') - return true; - - return false; - } - - #region Emit - private static Func CreateDeserializer(Type type, TypeInfo typeInfo) - where T : new() - { - var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true); - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - EmitReadValue(generator, type, typeInfo, true); - - generator.Emit(OpCodes.Ret); - return method.CreateDelegate(typeof(Func)) as Func; - } - private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) - { - //Convert enum types to their base type - if (typeInfo.IsEnum) - { - type = Enum.GetUnderlyingType(type); - typeInfo = type.GetTypeInfo(); - } - //Primitives/Enums - if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short), - typeof(ushort), typeof(int), typeof(uint), typeof(long), - typeof(ulong), typeof(double), typeof(bool), typeof(string), - typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), - typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), - typeof(bool?), typeof(float?), typeof(double?) - /*typeof(object), typeof(DateTime)*/)) - { - //No conversion needed - generator.EmitCall(OpCodes.Call, GetReadMethod(type), null); - } - //Dictionaries - /*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Enumerable - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Nullable Structs - else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && - typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) - { - generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Structs/Classes - else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) - { - if (isTop) - { - typeInfo.ForEachField(f => - { - string name; - if (!f.IsPublic || !IsETFProperty(f, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj - generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue - EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); - }); - - typeInfo.ForEachProperty(p => - { - string name; - if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj - generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue - EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); - }); - } - else - { - //While we could drill deeper and make a large serializer that also serializes all subclasses, - //it's more efficient to serialize on a per-type basis via another Write call. - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - }*/ - //Unsupported (decimal, char) - else - throw new InvalidOperationException($"Deserializing {type.Name} is not supported."); - } - - private static bool IsType(Type type, params Type[] types) - { - for (int i = 0; i < types.Length; i++) - { - if (type == types[i]) - return true; - } - return false; - } - private static bool IsETFProperty(FieldInfo f, out string name) - { - var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); - if (attrib != null) - { - name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; - return true; - } - name = null; - return false; - } - private static bool IsETFProperty(PropertyInfo p, out string name) - { - var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); - if (attrib != null) - { - name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; - return true; - } - name = null; - return false; - } - - private static MethodInfo GetReadMethod(string name) - => typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single(); - private static MethodInfo GetReadMethod(Type type) - { - MethodInfo method; - if (_readMethods.TryGetValue(type, out method)) - return method; - return null; - } - private static Dictionary GetPrimitiveReadMethods() - { - return new Dictionary - { - { typeof(bool), GetReadMethod(nameof(ReadBool)) }, - { typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) }, - { typeof(byte), GetReadMethod(nameof(ReadByte)) }, - { typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) }, - { typeof(sbyte), GetReadMethod(nameof(ReadSByte)) }, - { typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) }, - { typeof(short), GetReadMethod(nameof(ReadShort)) }, - { typeof(short?), GetReadMethod(nameof(ReadNullableShort)) }, - { typeof(ushort), GetReadMethod(nameof(ReadUShort)) }, - { typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) }, - { typeof(int), GetReadMethod(nameof(ReadInt)) }, - { typeof(int?), GetReadMethod(nameof(ReadNullableInt)) }, - { typeof(uint), GetReadMethod(nameof(ReadUInt)) }, - { typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) }, - { typeof(long), GetReadMethod(nameof(ReadLong)) }, - { typeof(long?), GetReadMethod(nameof(ReadNullableLong)) }, - { typeof(ulong), GetReadMethod(nameof(ReadULong)) }, - { typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) }, - { typeof(float), GetReadMethod(nameof(ReadSingle)) }, - { typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) }, - { typeof(double), GetReadMethod(nameof(ReadDouble)) }, - { typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) }, - }; - } - #endregion - - #region IDisposable - private bool _isDisposed = false; - - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - if (_leaveOpen) - _stream.Flush(); - else - _stream.Dispose(); - } - _isDisposed = true; - } - } - - public void Dispose() => Dispose(true); - #endregion - } -} \ No newline at end of file diff --git a/src/Discord.Net/ETF/ETFType.cs b/src/Discord.Net/ETF/ETFType.cs deleted file mode 100644 index 53499d5fa..000000000 --- a/src/Discord.Net/ETF/ETFType.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Discord.ETF -{ - public enum ETFType : byte - { - NEW_FLOAT_EXT = 70, - BIT_BINARY_EXT = 77, - ATOM_CACHE_REF = 82, - SMALL_INTEGER_EXT = 97, - INTEGER_EXT = 98, - FLOAT_EXT = 99, - ATOM_EXT = 100, - REFERENCE_EXT = 101, - PORT_EXT = 102, - PID_EXT = 103, - SMALL_TUPLE_EXT = 104, - LARGE_TUPLE_EXT = 105, - NIL_EXT = 106, - STRING_EXT = 107, - LIST_EXT = 108, - BINARY_EXT = 109, - SMALL_BIG_EXT = 110, - LARGE_BIG_EXT = 111, - NEW_FUN_EXT = 112, - EXPORT_EXT = 113, - NEW_REFERENCE_EXT = 114, - SMALL_ATOM_EXT = 115, - MAP_EXT = 116, - FUN_EXT = 117, - ATOM_UTF8_EXT = 118, - SMALL_ATOM_UTF8_EXT = 119 - } -} diff --git a/src/Discord.Net/ETF/ETFWriter.cs b/src/Discord.Net/ETF/ETFWriter.cs deleted file mode 100644 index 37d1553db..000000000 --- a/src/Discord.Net/ETF/ETFWriter.cs +++ /dev/null @@ -1,482 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; - -namespace Discord.ETF -{ - public unsafe class ETFWriter : IDisposable - { - private static readonly ConcurrentDictionary _serializers = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary _indirectSerializers = new ConcurrentDictionary(); - - private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; - private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; - private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; - - private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null); - private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>)); - private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>)); - private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>)); - private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - private readonly Stream _stream; - private readonly byte[] _buffer; - private readonly bool _leaveOpen; - private readonly Encoding _encoding; - - public virtual Stream BaseStream - { - get - { - Flush(); - return _stream; - } - } - - public ETFWriter(Stream stream, bool leaveOpen = false) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - - _stream = stream; - _leaveOpen = leaveOpen; - _buffer = new byte[11]; - _encoding = Encoding.UTF8; - } - - public void Write(bool value) - { - if (value) - _stream.Write(_trueBytes, 0, _trueBytes.Length); - else - _stream.Write(_falseBytes, 0, _falseBytes.Length); - } - public void Write(sbyte value) => Write((long)value); - public void Write(byte value) => Write((ulong)value); - public void Write(short value) => Write((long)value); - public void Write(ushort value) => Write((ulong)value); - public void Write(int value) => Write((long)value); - public void Write(uint value) => Write((ulong)value); - public void Write(long value) - { - if (value >= byte.MinValue && value <= byte.MaxValue) - { - _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; - _buffer[1] = (byte)value; - _stream.Write(_buffer, 0, 2); - } - else if (value >= int.MinValue && value <= int.MaxValue) - { - //TODO: Does this encode negatives correctly? - _buffer[0] = (byte)ETFType.INTEGER_EXT; - _buffer[1] = (byte)(value >> 24); - _buffer[2] = (byte)(value >> 16); - _buffer[3] = (byte)(value >> 8); - _buffer[4] = (byte)value; - _stream.Write(_buffer, 0, 5); - } - else - { - _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; - if (value < 0) - { - _buffer[2] = 1; //Is negative - value = -value; - } - - byte bytes = 0; - while (value > 0) - _buffer[3 + bytes++] = (byte)(value >>= 8); - _buffer[1] = bytes; //Encoded bytes - - _stream.Write(_buffer, 0, 3 + bytes); - } - } - public void Write(ulong value) - { - if (value <= byte.MaxValue) - { - _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; - _buffer[1] = (byte)value; - _stream.Write(_buffer, 0, 2); - } - else if (value <= int.MaxValue) - { - _buffer[0] = (byte)ETFType.INTEGER_EXT; - _buffer[1] = (byte)(value >> 24); - _buffer[2] = (byte)(value >> 16); - _buffer[3] = (byte)(value >> 8); - _buffer[4] = (byte)value; - _stream.Write(_buffer, 0, 5); - } - else - { - _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; - _buffer[2] = 0; //Always positive - - byte bytes = 0; - while (value > 0) - _buffer[3 + bytes++] = (byte)(value >>= 8); - _buffer[1] = bytes; //Encoded bytes - - _stream.Write(_buffer, 0, 3 + bytes); - } - } - - public void Write(float value) => Write((double)value); - public void Write(double value) - { - ulong value2 = *(ulong*)&value; - _buffer[0] = (byte)ETFType.NEW_FLOAT_EXT; - _buffer[1] = (byte)(value2 >> 56); - _buffer[2] = (byte)(value2 >> 48); - _buffer[3] = (byte)(value2 >> 40); - _buffer[4] = (byte)(value2 >> 32); - _buffer[5] = (byte)(value2 >> 24); - _buffer[6] = (byte)(value2 >> 16); - _buffer[7] = (byte)(value2 >> 8); - _buffer[8] = (byte)value2; - _stream.Write(_buffer, 0, 9); - } - - public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond)); - - public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } - public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } - public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } - public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } - public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } - public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } - public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - - public void Write(string value) - { - if (value != null) - { - var bytes = _encoding.GetBytes(value); - int count = bytes.Length; - _buffer[0] = (byte)ETFType.BINARY_EXT; - _buffer[1] = (byte)(count >> 24); - _buffer[2] = (byte)(count >> 16); - _buffer[3] = (byte)(count >> 8); - _buffer[4] = (byte)count; - _stream.Write(_buffer, 0, 5); - _stream.Write(bytes, 0, bytes.Length); - } - else - WriteNil(); - } - public void Write(byte[] value) - { - if (value != null) - { - int count = value.Length; - _buffer[0] = (byte)ETFType.BINARY_EXT; - _buffer[1] = (byte)(count >> 24); - _buffer[2] = (byte)(count >> 16); - _buffer[3] = (byte)(count >> 8); - _buffer[4] = (byte)count; - _stream.Write(_buffer, 0, 5); - _stream.Write(value, 0, value.Length); - } - else - WriteNil(); - } - - public void Write(T obj) - { - var type = typeof(T); - var typeInfo = type.GetTypeInfo(); - var action = _serializers.GetOrAdd(type, _ => CreateSerializer(type, typeInfo, false)) as Action; - action(this, obj); - } - public void Write(T? obj) - where T : struct - { - if (obj != null) - Write(obj.Value); - else - WriteNil(); - } - public void Write(IEnumerable obj) - { - if (obj != null) - { - var array = obj.ToArray(); - int length = array.Length; - _buffer[0] = (byte)ETFType.LIST_EXT; - _buffer[1] = (byte)(length >> 24); - _buffer[2] = (byte)(length >> 16); - _buffer[3] = (byte)(length >> 8); - _buffer[4] = (byte)length; - _stream.Write(_buffer, 0, 5); - - for (int i = 0; i < array.Length; i++) - Write(array[i]); - - _buffer[0] = (byte)ETFType.NIL_EXT; - _stream.Write(_buffer, 0, 1); - } - else - WriteNil(); - } - public void Write(IDictionary obj) - { - if (obj != null) - { - int length = obj.Count; - _buffer[0] = (byte)ETFType.MAP_EXT; - _buffer[1] = (byte)(length >> 24); - _buffer[2] = (byte)(length >> 16); - _buffer[3] = (byte)(length >> 8); - _buffer[4] = (byte)length; - _stream.Write(_buffer, 0, 5); - - foreach (var pair in obj) - { - Write(pair.Key); - Write(pair.Value); - } - } - else - WriteNil(); - } - public void Write(object obj) - { - if (obj != null) - { - var type = obj.GetType(); - var typeInfo = type.GetTypeInfo(); - var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer(type, typeInfo, true)) as Action; - action(this, obj); - } - else - WriteNil(); - } - - private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); - - public virtual void Flush() => _stream.Flush(); - public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); - - #region Emit - private static Action CreateSerializer(Type type, TypeInfo typeInfo, bool isDirect) - { - var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF", - null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true); - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value - if (!isDirect) - { - if (typeInfo.IsValueType) //Unbox value types - generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value - else //Cast reference types - generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version - } - else - EmitWriteValue(generator, type, typeInfo, true); - - generator.Emit(OpCodes.Ret); - return method.CreateDelegate(typeof(Action)) as Action; - } - private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) - { - //Convert enum types to their base type - if (typeInfo.IsEnum) - { - type = Enum.GetUnderlyingType(type); - typeInfo = type.GetTypeInfo(); - } - - //Primitives/Enums - Type targetType = null; - if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string), - typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), - typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), - typeof(bool?), typeof(float?), typeof(double?), - typeof(object), typeof(DateTime))) - { - //No conversion needed - targetType = type; - } - else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) - { - //Convert to long - generator.Emit(OpCodes.Conv_I8); - targetType = typeof(long); - } - else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) - { - //Convert to ulong - generator.Emit(OpCodes.Conv_U8); - targetType = typeof(ulong); - } - else if (IsType(type, typeof(float))) - { - //Convert to double - generator.Emit(OpCodes.Conv_R8); - targetType = typeof(double); - } - if (targetType != null) - generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); - - //Dictionaries - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Enumerable - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Nullable Structs - else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && - typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) - { - generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Structs/Classes - else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) - { - if (isTop) - { - typeInfo.ForEachField(f => - { - string name; - if (!f.IsPublic || !IsETFProperty(f, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj - generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue - EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); - }); - - typeInfo.ForEachProperty(p => - { - string name; - if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj - generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue - EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); - }); - } - else - { - //While we could drill deeper and make a large serializer that also serializes all subclasses, - //it's more efficient to serialize on a per-type basis via another Write call. - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - } - //Unsupported (decimal, char) - else - throw new InvalidOperationException($"Serializing {type.Name} is not supported."); - } - - private static bool IsType(Type type, params Type[] types) - { - for (int i = 0; i < types.Length; i++) - { - if (type == types[i]) - return true; - } - return false; - } - private static bool IsETFProperty(FieldInfo f, out string name) - { - var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); - if (attrib != null) - { - name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; - return true; - } - name = null; - return false; - } - private static bool IsETFProperty(PropertyInfo p, out string name) - { - var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); - if (attrib != null) - { - name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; - return true; - } - name = null; - return false; - } - - private static MethodInfo GetWriteMethod(Type paramType) - { - return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) - .Where(x => x.GetParameters()[0].ParameterType == paramType) - .Single(); - } - private static MethodInfo GetGenericWriteMethod(Type genericType) - { - if (genericType == null) - { - return typeof(ETFWriter).GetTypeInfo() - .GetDeclaredMethods(nameof(Write)) - .Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) - .Single(); - } - else - { - return typeof(ETFWriter).GetTypeInfo() - .GetDeclaredMethods(nameof(Write)) - .Where(x => - { - if (!x.IsGenericMethodDefinition) return false; - var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); - return p.IsGenericType && p.GetGenericTypeDefinition() == genericType; - }) - .Single(); - } - } - #endregion - - #region IDisposable - private bool _isDisposed = false; - - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - if (_leaveOpen) - _stream.Flush(); - else - _stream.Dispose(); - } - _isDisposed = true; - } - } - - public void Dispose() => Dispose(true); - #endregion - } -} diff --git a/src/Discord.Net/Entities/Attachment.cs b/src/Discord.Net/Entities/Attachment.cs new file mode 100644 index 000000000..5f2c1ed47 --- /dev/null +++ b/src/Discord.Net/Entities/Attachment.cs @@ -0,0 +1,18 @@ +using Model = Discord.API.Attachment; + +namespace Discord +{ + public struct Attachment + { + public ulong Id { get; } + public int Size { get; } + public string Filename { get; } + + public Attachment(Model model) + { + Id = model.Id; + Size = model.Size; + Filename = model.Filename; + } + } +} diff --git a/src/Discord.Net/Entities/Channel.cs b/src/Discord.Net/Entities/Channel.cs deleted file mode 100644 index 5894ade45..000000000 --- a/src/Discord.Net/Entities/Channel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using System.Collections.Generic; - -namespace Discord -{ - public abstract class Channel : IChannel - { - /// An entry in a public channel's permissions that gives or takes permissions from a specific role or user. - public class PermissionRule - { - /// The type of object TargetId is referring to. - public PermissionTarget TargetType { get; } - /// The Id of an object, whos type is specified by TargetType, that is the target of permissions being added or taken away. - public ulong TargetId { get; } - /// A collection of permissions that are added or taken away from the target. - public ChannelTriStatePermissions Permissions { get; } - - internal PermissionRule(PermissionTarget targetType, ulong targetId, uint allow, uint deny) - { - TargetType = targetType; - TargetId = targetId; - Permissions = new ChannelTriStatePermissions(allow, deny); - } - } - - /// Gets the unique identifier for this channel. - public ulong Id { get; } - - public abstract DiscordClient Client { get; } - /// Gets the type of this channel. - public abstract ChannelType Type { get; } - public bool IsText => (Type & ChannelType.Text) != 0; - public bool IsVoice => (Type & ChannelType.Voice) != 0; - public bool IsPrivate => (Type & ChannelType.Private) != 0; - public bool IsPublic => (Type & ChannelType.Public) != 0; - - public abstract User CurrentUser { get; } - /// Gets a collection of all users in this channel. - public abstract IEnumerable Users { get; } - - internal abstract MessageManager MessageManager { get; } - internal abstract PermissionManager PermissionManager { get; } - - protected Channel(ulong id) - { - Id = id; - } - - internal abstract void Update(APIChannel model); - - internal abstract User GetUser(ulong id); - - internal abstract Channel Clone(); - } -} diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs new file mode 100644 index 000000000..f8ce92dfa --- /dev/null +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -0,0 +1,99 @@ +using Discord.API.Rest; +using Discord.Net; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class DMChannel : IEntity, IMessageChannel + { + /// + public ulong Id { get; } + /// + public DiscordClient Discord { get; } + + /// + public DMUser Recipient { get; private set; } + + /// + public string Name => $"@{Recipient.Username}#{Recipient.Discriminator}"; + /// + public IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + /// + ChannelType IChannel.Type => ChannelType.DM; + /// + IEnumerable IChannel.Users => Users; + + private readonly MessageManager _messages; + + internal DMChannel(ulong id, DiscordClient client, int messageCacheSize) + { + Id = id; + Discord = client; + _messages = new MessageManager(this, messageCacheSize); + } + internal void Update(Model model) + { + if (Recipient == null) + Recipient = Discord.CreateDMUser(this, model.Recipient); + else + Recipient.Update(model.Recipient); + } + + /// + public User GetUser(ulong id) + { + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser; + else + return null; + } + + /// + public Task GetMessage(ulong id) + => _messages.Get(id); + /// + public Task> GetMessages(int limit = 100) + => _messages.GetMany(limit); + /// + public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + => _messages.GetMany(limit, relativeMessageId, relativeDir); + + /// + public Task SendMessage(string text, bool isTTS = false) + => _messages.Send(text, isTTS); + /// + public Task SendFile(string filePath, string text = null, bool isTTS = false) + => _messages.SendFile(filePath, text, isTTS); + /// + public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + => _messages.SendFile(stream, filename, text, isTTS); + + /// + public Task TriggerTyping() + => _messages.TriggerTyping(); + + /// + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetChannelRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + /// + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs new file mode 100644 index 000000000..862c91671 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -0,0 +1,130 @@ +using Discord.API.Rest; +using Discord.Net; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public abstract class GuildChannel : IChannel, IEntity + { + private readonly PermissionManager _permissions; + + /// + public ulong Id { get; } + /// Gets the guild this channel is a member of. + public Guild Guild { get; } + /// + public abstract ChannelType Type { get; } + + /// + public string Name { get; private set; } + /// Gets the position of this public channel relative to others of the same type. + public int Position { get; private set; } + + /// + public DiscordClient Discord => Guild.Discord; + /// Gets a collection of all users in this channel. + public IEnumerable Users => _permissions.GetUsers(); + /// + IEnumerable IChannel.Users => _permissions.GetUsers(); + /// Gets a collection of permission overwrites for this channel. + public IEnumerable PermissionOverwrites => _permissions.Overwrites; + + internal GuildChannel(ulong id, Guild guild, bool usePermissionsCache) + { + Id = id; + Guild = guild; + + _permissions = new PermissionManager(this, usePermissionsCache); + } + + internal virtual void Update(Model model) + { + Name = model.Name; + Position = model.Position; + + _permissions.Update(model); + } + + /// Gets a user in this channel with the given id. + public GuildUser GetUser(ulong id) + => _permissions.GetUser(id); + /// + User IChannel.GetUser(ulong id) => GetUser(id); + + /// Gets the permission overwrite for a specific user, or null if one does not exist. + public OverwritePermissions? GetPermissionOverwrite(GuildUser user) + => _permissions.GetOverwrite(user); + /// Gets the permission overwrite for a specific role, or null if one does not exist. + public OverwritePermissions? GetPermissionOverwrite(Role role) + => _permissions.GetOverwrite(role); + /// Downloads a collection of all invites to this channel. + public async Task> GetInvites() + { + var response = await Discord.RestClient.Send(new GetChannelInvitesRequest(Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = Discord.CreateGuildInvite(this, x); + invite.Update(x); + return invite; + }); + } + + /// Adds or updates the permission overwrite for the given user. + public Task UpdatePermissionOverwrite(GuildUser user, OverwritePermissions permissions) + => _permissions.AddOrUpdateOverwrite(user, permissions); + /// Adds or updates the permission overwrite for the given role. + public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) + => _permissions.AddOrUpdateOverwrite(role, permissions); + /// Removes the permission overwrite for the given user, if one exists. + public Task RemovePermissionOverwrite(GuildUser user) + => _permissions.RemoveOverwrite(user); + /// Removes the permission overwrite for the given role, if one exists. + public Task RemovePermissionOverwrite(Role role) + => _permissions.RemoveOverwrite(role); + + internal ChannelPermissions GetPermissions(GuildUser user) + => _permissions.GetPermissions(user); + internal void UpdatePermissions() + => _permissions.UpdatePermissions(); + internal void UpdatePermissions(GuildUser user) + => _permissions.UpdatePermissions(user); + + /// Creates a new invite to this channel. + /// Time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// If true, a user accepting this invite will be kicked from the guild after closing their client. + /// If true, creates a human-readable link. Not supported if maxAge is set to null. + public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool humanReadable = false) + { + var response = await Discord.RestClient.Send(new CreateChannelInviteRequest(Id) + { + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + IsTemporary = tempMembership, + WithXkcdPass = humanReadable + }).ConfigureAwait(false); + return Discord.CreatePublicInvite(response); + } + + /// + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetChannelRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; + } +} diff --git a/ref/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs similarity index 80% rename from ref/Entities/Channels/IChannel.cs rename to src/Discord.Net/Entities/Channels/IChannel.cs index fb82fb30d..046718f24 100644 --- a/ref/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; namespace Discord { @@ -9,10 +8,10 @@ namespace Discord ChannelType Type { get; } /// Gets the name of this channel. string Name { get; } + /// Gets a collection of all users in this channel. + IEnumerable Users { get; } /// Gets a user in this channel with the given id. - Task GetUser(ulong id); - /// Gets a collection of all users in this channel. - Task> GetUsers(); + User GetUser(ulong id); } } diff --git a/ref/Entities/Channels/ITextChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs similarity index 95% rename from ref/Entities/Channels/ITextChannel.cs rename to src/Discord.Net/Entities/Channels/IMessageChannel.cs index f3701abbf..4bb8b5690 100644 --- a/ref/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - public interface ITextChannel : IChannel + public interface IMessageChannel : IChannel { /// Gets the message in this text channel with the given id, or null if none was found. Task GetMessage(ulong id); @@ -25,6 +25,6 @@ namespace Discord Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. - Task SendIsTyping(); + Task TriggerTyping(); } } diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs new file mode 100644 index 000000000..cebb1c573 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -0,0 +1,67 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class TextChannel : GuildChannel, IMessageChannel, IMentionable + { + private readonly MessageManager _messages; + + /// + public string Topic { get; private set; } + + /// + public override ChannelType Type => ChannelType.Text; + /// + public string Mention => MentionHelper.Mention(this); + + internal TextChannel(ulong id, Guild guild, int messageCacheSize, bool usePermissionsCache) + : base(id, guild, usePermissionsCache) + { + _messages = new MessageManager(this, messageCacheSize); + } + + internal override void Update(Model model) + { + Topic = model.Topic; + base.Update(model); + } + + /// + public Task GetMessage(ulong id) + => _messages.Get(id); + /// + public Task> GetMessages(int limit = 100) + => _messages.GetMany(limit); + /// + public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + => _messages.GetMany(limit, relativeMessageId, relativeDir); + + /// + public Task SendMessage(string text, bool isTTS = false) + => _messages.Send(text, isTTS); + /// + public Task SendFile(string filePath, string text = null, bool isTTS = false) + => _messages.SendFile(filePath, text, isTTS); + /// + public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + => _messages.SendFile(stream, filename, text, isTTS); + + /// + public Task TriggerTyping() + => _messages.TriggerTyping(); + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyTextChannelRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs new file mode 100644 index 000000000..80fbae797 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -0,0 +1,39 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class VoiceChannel : GuildChannel + { + /// + public int Bitrate { get; private set; } + + /// + public override ChannelType Type => ChannelType.Voice; + + internal VoiceChannel(ulong id, Guild guild, bool usePermissionsCache) + : base(id, guild, usePermissionsCache) + { + } + + internal override void Update(Model model) + { + Bitrate = model.Bitrate; + base.Update(model); + } + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyVoiceChannelRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; + } +} diff --git a/src/Discord.Net/Entities/Color.cs b/src/Discord.Net/Entities/Color.cs index 325aa2f39..3aecc39ac 100644 --- a/src/Discord.Net/Entities/Color.cs +++ b/src/Discord.Net/Entities/Color.cs @@ -1,24 +1,37 @@ namespace Discord { - public class Color + public class Color { + /// Gets the default user color value. public static readonly Color Default = new Color(0); - - public uint RawValue { get; } - - public Color(uint rawValue) { RawValue = rawValue; } - public Color(byte r, byte g, byte b) : this(((uint)r << 16) | ((uint)g << 8) | b) { } - public Color(float r, float g, float b) : this((byte)(r * 255.0f), (byte)(g * 255.0f), (byte)(b * 255.0f)) { } - /// Gets or sets the red component for this color. - public byte R => (byte)(RawValue >> 16); - /// Gets or sets the green component for this color. + /// Gets the encoded value for this color. + public uint RawValue { get; } + + /// Gets the red component for this color. + public byte R => (byte)(RawValue >> 16); + /// Gets the green component for this color. public byte G => (byte)(RawValue >> 8); - /// Gets or sets the blue component for this color. + /// Gets the blue component for this color. public byte B => (byte)(RawValue); - private byte GetByte(int pos) => (byte)(RawValue >> (8 * (pos - 1))); - - public override string ToString() => '#' + RawValue.ToString("X"); + public Color(uint rawValue) + { + RawValue = rawValue; + } + public Color(byte r, byte g, byte b) + { + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + b; + } + public Color(float r, float g, float b) + { + RawValue = + ((uint)(r * 255.0f) << 16) | + ((uint)(g * 255.0f) << 8) | + (uint)(b * 255.0f); + } } } diff --git a/src/Discord.Net/Entities/Embed.cs b/src/Discord.Net/Entities/Embed.cs new file mode 100644 index 000000000..271e47f66 --- /dev/null +++ b/src/Discord.Net/Entities/Embed.cs @@ -0,0 +1,25 @@ +using Model = Discord.API.Embed; + +namespace Discord +{ + public struct Embed + { + public string Url { get; } + public string Type { get; } + public string Title { get; } + public string Description { get; } + public EmbedProvider Provider { get; } + public EmbedThumbnail Thumbnail { get; } + + internal Embed(Model model) + { + Url = model.Url; + Type = model.Type; + Title = model.Title; + Description = model.Description; + + Provider = new EmbedProvider(model.Provider); + Thumbnail = new EmbedThumbnail(model.Thumbnail); + } + } +} diff --git a/src/Discord.Net/Entities/EmbedProvider.cs b/src/Discord.Net/Entities/EmbedProvider.cs new file mode 100644 index 000000000..2fce8dfe7 --- /dev/null +++ b/src/Discord.Net/Entities/EmbedProvider.cs @@ -0,0 +1,16 @@ +using Model = Discord.API.EmbedProvider; + +namespace Discord +{ + public struct EmbedProvider + { + public string Name { get; } + public string Url { get; } + + internal EmbedProvider(Model model) + { + Name = model.Name; + Url = model.Url; + } + } +} diff --git a/src/Discord.Net/Entities/EmbedThumbnail.cs b/src/Discord.Net/Entities/EmbedThumbnail.cs new file mode 100644 index 000000000..a61323ed6 --- /dev/null +++ b/src/Discord.Net/Entities/EmbedThumbnail.cs @@ -0,0 +1,20 @@ +using Model = Discord.API.EmbedThumbnail; + +namespace Discord +{ + public struct EmbedThumbnail + { + public string Url { get; } + public string ProxyUrl { get; } + public int? Height { get; } + public int? Width { get; } + + internal EmbedThumbnail(Model model) + { + Url = model.Url; + ProxyUrl = model.ProxyUrl; + Height = model.Height; + Width = model.Width; + } + } +} diff --git a/src/Discord.Net/Entities/Emoji.cs b/src/Discord.Net/Entities/Emoji.cs new file mode 100644 index 000000000..66010d968 --- /dev/null +++ b/src/Discord.Net/Entities/Emoji.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Emoji; + +namespace Discord +{ + public struct Emoji + { + private readonly Guild _guild; + private readonly ImmutableArray _roles; + + public ulong Id { get; } + public string Name { get; } + public bool IsManaged { get; } + public bool RequireColons { get; } + + internal Emoji(Model model, Guild guild) + { + Id = model.Id; + _guild = guild; + + Name = model.Name; + IsManaged = model.Managed; + RequireColons = model.RequireColons; + _roles = model.Roles.Select(x => guild.GetRole(x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net/Entities/Guild.cs b/src/Discord.Net/Entities/Guild.cs new file mode 100644 index 000000000..62172f47d --- /dev/null +++ b/src/Discord.Net/Entities/Guild.cs @@ -0,0 +1,383 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Guild; + +namespace Discord +{ + /// Represents a Discord guild (called a server in the official client). + public class Guild : IEntity + { + private struct Member + { + public readonly GuildUser User; + public readonly GuildPermissions Permissions; + public Member(GuildUser user, GuildPermissions permissions) + { + User = user; + Permissions = permissions; + } + } + + private ConcurrentDictionary _channels; + private ConcurrentDictionary _members; + private ConcurrentDictionary _presences; + private ConcurrentDictionary _roles; + private ConcurrentDictionary _voiceStates; + private ulong _ownerId; + private ulong? _afkChannelId, _embedChannelId; + private int _userCount; + private string _iconId, _splashId; + + /// + public ulong Id { get; } + /// + public DiscordClient Discord { get; } + public GuildUser CurrentUser { get; } + + /// Gets the name of this guild. + public string Name { get; private set; } + /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. + public int AFKTimeout { get; private set; } + /// Returns true if this guild is embeddable (e.g. widget) + public bool IsEmbeddable { get; private set; } + + /// Gets a list of all custom emojis for this guild. + public IReadOnlyList Emojis { get; private set; } + /// Gets a list of extra features added to this guild. + public IReadOnlyList Features { get; private set; } + + /// Gets the voice region for this guild. + public VoiceRegion Region { get; private set; } + /*/// Gets the date and time you joined this guild. + public DateTime JoinedAt { get; private set; }*/ + /// Gets the the role representing all users in a guild. + public Role EveryoneRole { get; private set; } + + /// Gets the number of channels in this guild. + public int ChannelCount => _channels.Count; + /// Gets the number of roles in this guild. + public int RoleCount => _roles.Count; + /// Gets the number of users in this guild. + public int UserCount => _userCount; + /// Gets the number of users downloaded for this guild so far. + internal int CurrentUserCount => _members.Count; + + /// Gets the URL to this guild's current icon. + public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); + /// Gets the URL to this guild's splash image. + public string SplashUrl => CDN.GetGuildSplashUrl(Id, _splashId); + + /// Gets the user that created this guild. + public GuildUser Owner => GetUser(_ownerId); + /// Gets the default channel for this guild. + public TextChannel DefaultChannel => GetChannel(Id) as TextChannel; + /// Gets the AFK voice channel for this guild. + public VoiceChannel AFKChannel => GetChannel(_afkChannelId) as VoiceChannel; + /// Gets the embed channel for this guild. + public IChannel EmbedChannel => GetChannel(_embedChannelId); //TODO: Is this text or voice? + /// Gets a collection of all channels in this guild. + public IEnumerable Channels => _channels.Select(x => x.Value); + /// Gets a collection of text channels in this guild. + public IEnumerable TextChannels => _channels.Select(x => x.Value).OfType(); + /// Gets a collection of voice channels in this guild. + public IEnumerable VoiceChannels => _channels.Select(x => x.Value).OfType(); + /// Gets a collection of all members in this guild. + public IEnumerable Users => _members.Select(x => x.Value.User); + /// Gets a collection of all roles in this guild. + public IEnumerable Roles => _roles.Select(x => x.Value); + + internal Guild(ulong id, DiscordClient client) + { + Id = id; + Discord = client; + + _channels = new ConcurrentDictionary(); + _members = new ConcurrentDictionary(); + _presences = new ConcurrentDictionary(); + _roles = new ConcurrentDictionary(); + _voiceStates = new ConcurrentDictionary(); + } + + internal void Update(Model model) + { + Name = model.Name; + AFKTimeout = model.AFKTimeout; + _ownerId = model.OwnerId; + _afkChannelId = model.AFKChannelId; + Region = Discord.GetVoiceRegion(model.Region); + _iconId = model.Icon; + _splashId = model.Splash; + Features = model.Features; + IsEmbeddable = model.EmbedEnabled; + _embedChannelId = model.EmbedChannelId; + _userCount = 0;// model.UserCount; + + _roles = new ConcurrentDictionary(2, model.Roles.Length); + foreach (var x in model.Roles) + _roles[x.Id] = Discord.CreateRole(this, x); + EveryoneRole = _roles[Id]; + + Emojis = model.Emojis.Select(x => new Emoji(x, this)).ToArray(); + } + /*internal void Update(ExtendedModel model) + { + Update(model as Model); + + //Only channels or members should have AddXXX(cachePerms: true), not both + if (model.Channels != null) + { + _channels = new ConcurrentDictionary(2, (int)(model.Channels.Length * 1.05)); + foreach (var subModel in model.Channels) + AddChannel(subModel.Id, false).Update(subModel); + DefaultChannel = _channels[Id]; + } + if (model.MemberCount != null) + { + if (_users == null) + _users = new ConcurrentDictionary(2, (int)(model.MemberCount * 1.05)); + _userCount = model.MemberCount.Value; + } + if (!model.IsLarge) + { + if (model.Members != null) + { + foreach (var subModel in model.Members) + AddUser(subModel.User.Id, true, false).Update(subModel); + } + if (model.VoiceStates != null) + { + foreach (var subModel in model.VoiceStates) + GetUser(subModel.UserId)?.Update(subModel); + } + if (model.Presences != null) + { + foreach (var subModel in model.Presences) + GetUser(subModel.User.Id)?.Update(subModel); + } + } + }*/ + + /// Gets the channel with the given id, or null if not found. + public GuildChannel GetChannel(ulong id) + { + GuildChannel result; + _channels.TryGetValue(id, out result); + return result; + } + /// Gets the channel refered to by the given mention, or null if not found. + public GuildChannel GetChannel(string mention) => GetChannel(MentionHelper.GetChannelId(mention)); + private GuildChannel GetChannel(ulong? id) => id != null ? GetChannel(id.Value) : null; + + /// Gets the channel with the given id, or null if not found. + public Role GetRole(ulong id) + { + Role result; + _roles.TryGetValue(id, out result); + return result; + } + private Role GetRole(ulong? id) => id != null ? GetRole(id.Value) : null; + + public GuildUser GetUser(ulong id) + { + Member result; + if (_members.TryGetValue(id, out result)) + return result.User; + else + return null; + } + public GuildUser GetUser(string username, ushort discriminator) + { + if (username == null) throw new ArgumentNullException(nameof(username)); + + foreach (var member in _members) + { + var user = member.Value.User; + if (user.Discriminator == discriminator && user.Username == username) + return user; + } + return null; + } + public GuildUser GetUser(string mention) => GetUser(MentionHelper.GetUserId(mention)); + private GuildUser GetUser(ulong? id) => id != null ? GetUser(id.Value) : null; + + public async Task> GetBans() + { + var discord = Discord; + var response = await Discord.RestClient.Send(new GetGuildBansRequest(Id)).ConfigureAwait(false); + return response.Select(x => Discord.CreateBannedUser(this, x)); + } + + public async Task> GetInvites() + { + var response = await Discord.RestClient.Send(new GetGuildInvitesRequest(Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = Discord.CreatePublicInvite(x); + invite.Update(x); + return invite; + }); + } + + public async Task CreateTextChannel(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var request = new CreateChannelRequest(Id) { Name = name, Type = ChannelType.Text }; + var response = await Discord.RestClient.Send(request).ConfigureAwait(false); + + return Discord.CreateTextChannel(this, response); + } + public async Task CreateVoiceChannel(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var request = new CreateChannelRequest(Id) { Name = name, Type = ChannelType.Voice }; + var response = await Discord.RestClient.Send(request).ConfigureAwait(false); + + return Discord.CreateVoiceChannel(this, response); + } + public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) + { + return DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); + } + public async Task CreateRole(string name, GuildPermissions? permissions = null, Color color = null, bool isHoisted = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var createRequest = new CreateRoleRequest(Id); + var createResponse = await Discord.RestClient.Send(createRequest).ConfigureAwait(false); + var role = Discord.CreateRole(this, createResponse); + + var editRequest = new ModifyGuildRoleRequest(role.Guild.Id, role.Id) + { + Name = name, + Permissions = (int)(permissions ?? role.Permissions).RawValue, + Color = (int)(color ?? Color.Default).RawValue, + Hoist = isHoisted + }; + var editResponse = await Discord.RestClient.Send(editRequest).ConfigureAwait(false); + role.Update(editResponse); + + return role; + } + + public async Task PruneUsers(int days = 30, bool simulate = false) + { + if (simulate) + { + var response = await Discord.RestClient.Send(new GetGuildPruneCountRequest(Id) { Days = days }).ConfigureAwait(false); + return response.Pruned; + } + else + { + var response = await Discord.RestClient.Send(new BeginGuildPruneRequest(Id) { Days = days }).ConfigureAwait(false); + return response.Pruned; + } + } + + public Task Ban(GuildUser user, int pruneDays = 0) + { + return Discord.RestClient.Send(new CreateGuildBanRequest(Id, user.Id) + { + PruneDays = pruneDays + }); + } + public async Task Unban(GuildUser user) + { + try { await Discord.RestClient.Send(new RemoveGuildBanRequest(Id, user.Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetGuildRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyGuildRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + + /// Leaves this guild. + public async Task Leave() + { + try { await Discord.RestClient.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// Deletes this guild. + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + internal void UpdatePermissions(GuildUser user) + { + Member member; + if (_members.TryGetValue(user.Id, out member)) + { + var perms = member.Permissions; + if (UpdatePermissions(member.User, ref perms)) + { + _members[user.Id] = new Member(member.User, perms); + foreach (var channel in _channels) + channel.Value.UpdatePermissions(user); + } + } + } + + private bool UpdatePermissions(GuildUser user, ref GuildPermissions permissions) + { + uint newPermissions = 0; + + if (user.Id == _ownerId) + newPermissions = GuildPermissions.All.RawValue; + else + { + foreach (var role in user.Presence.Roles) + newPermissions |= role.Permissions.RawValue; + } + + if (PermissionsHelper.HasBit(ref newPermissions, (byte)PermissionBit.ManageRolesOrPermissions)) + newPermissions = GuildPermissions.All.RawValue; + + if (newPermissions != permissions.RawValue) + { + permissions = new GuildPermissions(newPermissions); + return true; + } + return false; + } + + /*internal IGuildChannel AddChannel(ulong id, bool cachePerms) + { + var channel = new Channel(Discord, id, this); + if (cachePerms && Discord.UsePermissionsCache) + { + foreach (var user in Users) + channel.AddUser(user); + } + Discord.AddChannel(channel); + return _channels.GetOrAdd(id, x => channel); + } + internal IGuildChannel RemoveChannel(ulong id) + { + IGuildChannel channel; + _channels.TryRemove(id, out channel); + return channel; + }*/ + } +} diff --git a/src/Discord.Net/Entities/Helpers/InviteManager.cs b/src/Discord.Net/Entities/Helpers/InviteManager.cs new file mode 100644 index 000000000..13058e621 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/InviteManager.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Discord +{ + public class InviteManager + { + } +} diff --git a/src/Discord.Net/Entities/Helpers/MentionHelper.cs b/src/Discord.Net/Entities/Helpers/MentionHelper.cs new file mode 100644 index 000000000..78777a518 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/MentionHelper.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Discord +{ + internal static class MentionHelper + { + private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>"); + private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>"); + private static readonly Regex _roleRegex = new Regex(@"@everyone"); + + internal static string Mention(User user) => $"<@{user.Id}>"; + internal static string Mention(IChannel channel) => $"<#{channel.Id}>"; + internal static string Mention(Role role) => role.IsEveryone ? "@everyone" : ""; + + internal static string CleanUserMentions(GuildChannel channel, string text, ImmutableArray.Builder users = null) + { + var guild = channel.Guild; + return _userRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var user = guild.GetUser(id); //We're able to mention users outside of our channel + if (user != null) + { + if (users != null) + users.Add(user); + return '@' + user.Username; + } + } + return e.Value; //User not found or parse failed + })); + } + internal static string CleanChannelMentions(GuildChannel channel, string text, ImmutableArray.Builder channels = null) + { + var guild = channel.Guild; + return _channelRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var mentionedChannel = guild.GetChannel(id); + if (mentionedChannel != null && mentionedChannel.Guild.Id == guild.Id) + { + if (channels != null) + channels.Add(mentionedChannel); + return '#' + mentionedChannel.Name; + } + } + return e.Value; //Channel not found or parse failed + })); + } + /*internal static string CleanRoleMentions(User user, IPublicChannel channel, string text, ImmutableArray.Builder roles = null) + { + var guild = channel.Guild; + if (guild == null) return text; + + return _roleRegex.Replace(text, new MatchEvaluator(e => + { + if (roles != null && user.GetPermissions(channel).MentionEveryone) + roles.Add(guild.EveryoneRole); + return e.Value; + })); + }*/ + + internal static ulong GetUserId(string mention) + { + mention = mention.Trim(); + if (mention.Length >= 3 && mention[0] == '<' && mention[1] == '@' && mention[mention.Length - 1] == '>') + { + mention = mention.Substring(2, mention.Length - 3); + + ulong id; + if (ulong.TryParse(mention, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + return id; + } + throw new ArgumentException("Invalid mention format", nameof(mention)); + } + internal static ulong GetChannelId(string mention) + { + mention = mention.Trim(); + if (mention.Length >= 3 && mention[0] == '<' && mention[1] == '#' && mention[mention.Length - 1] == '>') + { + mention = mention.Substring(2, mention.Length - 3); + + ulong id; + if (ulong.TryParse(mention, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + return id; + } + throw new ArgumentException("Invalid mention format", nameof(mention)); + } + + internal static string ResolveMentions(IChannel channel, string text) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (text == null) throw new ArgumentNullException(nameof(text)); + + var publicChannel = channel as GuildChannel; + if (publicChannel != null) + { + text = CleanUserMentions(publicChannel, text); + text = CleanChannelMentions(publicChannel, text); + //text = CleanRoleMentions(publicChannel, text); + } + return text; + } + } +} diff --git a/src/Discord.Net/Entities/Helpers/MessageManager.cs b/src/Discord.Net/Entities/Helpers/MessageManager.cs new file mode 100644 index 000000000..1285093ae --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/MessageManager.cs @@ -0,0 +1,174 @@ +using Discord.API.Rest; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord +{ + internal class MessageManager : IEnumerable + { + private readonly IMessageChannel _channel; + private readonly ConcurrentDictionary _messages; + private readonly ConcurrentQueue _orderedMessages; + private readonly int _size; + + public MessageManager(IMessageChannel channel, int size = 0) + { + _channel = channel; + _size = (int)(size * 1.05); + if (size > 0) + { + _messages = new ConcurrentDictionary(2, size); + _orderedMessages = new ConcurrentQueue(); + } + } + + internal Message Add(Model model, User user) + => Add(_channel.Discord.CreateMessage(_channel, user, model)); + private Message Add(Message message) + { + if (_size > 0) + { + if (_messages.TryAdd(message.Id, message)) + { + _orderedMessages.Enqueue(message); + + Message msg; + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msg)) + _messages.TryRemove(msg.Id, out msg); + } + } + return message; + } + internal void Remove(ulong id) + { + if (_size > 0) + { + Message msg; + _messages.TryRemove(id, out msg); + } + } + + public Task Get(ulong id) + { + if (_messages != null) + { + Message result; + if (_messages.TryGetValue(id, out result)) + return Task.FromResult(result); + } + else + throw new NotSupportedException(); //TODO: Not supported yet + + return Task.FromResult(null); + } + + public async Task> GetMany(int limit = DiscordConfig.MaxMessagesPerBatch, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + { + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) return ImmutableArray.Empty; + + if (_messages != null) + { + ImmutableArray cachedMessages; + if (relativeMessageId == null) + cachedMessages = _orderedMessages.ToImmutableArray(); + else if (relativeDir == Relative.Before) + cachedMessages = _orderedMessages.Where(x => x.Id < relativeMessageId.Value).ToImmutableArray(); + else + cachedMessages = _orderedMessages.Where(x => x.Id > relativeMessageId.Value).ToImmutableArray(); + + if (cachedMessages.Length == limit) + return cachedMessages; + else if (cachedMessages.Length > limit) + return cachedMessages.Skip(cachedMessages.Length - limit); + else + { + var missingMessages = await Download(limit - cachedMessages.Length, cachedMessages[0].Id, Relative.Before).ConfigureAwait(false); + return missingMessages.SelectMany(x => x).Concat(cachedMessages); + } + } + return (await Download(limit, relativeMessageId, relativeDir).ConfigureAwait(false)).SelectMany(x => x); + } + private async Task>> Download(int limit = DiscordConfig.MaxMessagesPerBatch, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + { + var request = new GetChannelMessagesRequest(_channel.Id) + { + Limit = limit, + RelativeDir = relativeDir, + RelativeId = relativeMessageId ?? 0 + }; + var guild = (_channel as GuildChannel)?.Guild; + var recipient = (_channel as DMChannel)?.Recipient; + + int runs = limit / DiscordConfig.MaxMessagesPerBatch; + int lastRunCount = limit - runs * DiscordConfig.MaxMessagesPerBatch; + var result = new Message[runs][]; + + int i = 0; + for (; i < runs; i++) + { + request.Limit = (i == runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; + + Model[] models = await _channel.Discord.RestClient.Send(request).ConfigureAwait(false); + + //Was this an empty batch? + if (models.Length == 0) break; + + Message[] msgs = new Message[models.Length]; + for (int j = 0; j < models.Length; j++) + { + var model = models[j]; + var user = _channel.GetUser(model.Author.Id); + msgs[j] = _channel.Discord.CreateMessage(_channel, user, model); + } + result[i] = msgs; + + request.RelativeId = relativeDir == Relative.Before ? msgs[0].Id : msgs[msgs.Length - 1].Id; + + //Was this an incomplete (the last) batch? + if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } + } + + //Dont return nulls if we didnt get all the requested messages + for (; i < runs; i++) + result[i] = Array.Empty(); + + return result; + } + + public Task Send(string text, bool isTTS) + { + if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); + if (text.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); + return _channel.Discord.MessageQueue.QueueSend(_channel, text, isTTS); + } + public async Task SendFile(string filePath, string text = null, bool isTTS = false) + { + using (var stream = File.OpenRead(filePath)) + return await SendFile(stream, Path.GetFileName(filePath), text, isTTS).ConfigureAwait(false); + } + public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + { + var request = new SendFileRequest(_channel.Id) + { + Filename = filename, + Stream = stream + }; + var response = await _channel.Discord.RestClient.Send(request).ConfigureAwait(false); + + return _channel.Discord.CreateMessage(_channel, _channel.GetCurrentUser(), response); + } + public Task TriggerTyping() => _channel.Discord.RestClient.Send(new TriggerTypingIndicatorRequest(_channel.Id)); + + public IEnumerator GetEnumerator() => _orderedMessages.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _orderedMessages.GetEnumerator(); + } +} diff --git a/src/Discord.Net/Entities/Helpers/PermissionManager.cs b/src/Discord.Net/Entities/Helpers/PermissionManager.cs new file mode 100644 index 000000000..a7047bd94 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/PermissionManager.cs @@ -0,0 +1,274 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + internal class PermissionManager + { + public struct Member + { + public GuildUser User { get; } + public ChannelPermissions Permissions { get; } + + public Member(GuildUser user, ChannelPermissions permissions) + { + User = user; + Permissions = permissions; + } + } + + private readonly GuildChannel _channel; + private readonly ConcurrentDictionary _users; + private Dictionary _rules; + + public IEnumerable Users => _users.Select(x => x.Value); + public IEnumerable Overwrites => _rules.Values; + + public PermissionManager(GuildChannel channel, bool cacheUsers) + { + _channel = channel; + if (cacheUsers) + _users = new ConcurrentDictionary(2, (int)(channel.Guild.UserCount * 1.05)); + } + + public void Update(Model model) + { + _rules = model.PermissionOverwrites + .Select(x => new Overwrite(x)) + .ToDictionary(x => x.TargetId); + UpdatePermissions(); + } + + public OverwritePermissions? GetOverwrite(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + Overwrite rule; + if (_rules.TryGetValue(user.Id, out rule)) + return rule.Permissions; + return null; + } + public OverwritePermissions? GetOverwrite(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + Overwrite rule; + if (_rules.TryGetValue(role.Id, out rule)) + return rule.Permissions; + return null; + } + public Task AddOrUpdateOverwrite(GuildUser user, OverwritePermissions permissions) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + return AddOrUpdateOverwrite(user.Id, permissions); + } + public Task AddOrUpdateOverwrite(Role role, OverwritePermissions permissions) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + return AddOrUpdateOverwrite(role.Id, permissions); + } + private Task AddOrUpdateOverwrite(ulong targetId, OverwritePermissions permissions) + { + var request = new ModifyChannelPermissionsRequest(_channel.Id, targetId) + { + Allow = permissions.AllowValue, + Deny = permissions.DenyValue + }; + return _channel.Discord.RestClient.Send(request); + } + public Task RemoveOverwrite(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + return RemoveOverwrite(user.Id); + } + public Task RemoveOverwrite(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + return RemoveOverwrite(role.Id); + } + private async Task RemoveOverwrite(ulong id) + { + try { await _channel.Discord.RestClient.Send(new DeleteChannelPermissionsRequest(_channel.Id, id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public ChannelPermissions GetPermissions(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + Member member; + if (_users.TryGetValue(user.Id, out member)) + return member.Permissions; + else + return ChannelPermissions.None; + } + else + { + var perms = new ChannelPermissions(); + ResolvePermissions(user, ref perms); + return perms; + } + } + public void UpdatePermissions() + { + if (_users != null) + { + foreach (var pair in _users) + { + var member = pair.Value; + var perms = member.Permissions; + if (ResolvePermissions(member.User, ref perms)) + _users[pair.Key] = new Member(member.User, perms); + } + } + } + public void UpdatePermissions(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + Member member; + if (_users.TryGetValue(user.Id, out member)) + { + var perms = member.Permissions; + if (ResolvePermissions(member.User, ref perms)) + _users[user.Id] = new Member(member.User, perms); + } + } + } + + + public ChannelPermissions ResolvePermissions(GuildUser user) + { + var permissions = new ChannelPermissions(); + ResolvePermissions(user, ref permissions); + return permissions; + } + public bool ResolvePermissions(GuildUser user, ref ChannelPermissions permissions) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + uint newPermissions = 0; + var guild = user.Guild; + + uint mask = ChannelPermissions.All(_channel.Type).RawValue; + if (user == guild.Owner) + newPermissions = mask; //Private messages and owners always have all permissions + else + { + //Start with this user's guild permissions + newPermissions = user.GuildPermissions.RawValue; + var rules = _rules; + + Overwrite entry; + var roles = user.Presence.Roles.ToArray(); + if (roles.Length > 0) + { + for (int i = 0; i < roles.Length; i++) + { + if (rules.TryGetValue(roles[i].Id, out entry)) + newPermissions &= ~entry.Permissions.DenyValue; + } + for (int i = 0; i < roles.Length; i++) + { + if (rules.TryGetValue(roles[i].Id, out entry)) + newPermissions |= entry.Permissions.AllowValue; + } + } + if (rules.TryGetValue(user.Id, out entry)) + newPermissions = (newPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; + + if (PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.ManageRolesOrPermissions)) + newPermissions = mask; //ManageRolesOrPermissions gives all permisions + else + { + var channelType = _channel.Type; + if (channelType == ChannelType.Text && !PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.ReadMessages)) + newPermissions = 0; //No read permission on a text channel removes all other permissions + else if (channelType == ChannelType.Voice && !PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.Connect)) + newPermissions = 0; //No connect permissions on a voice channel removes all other permissions + else + newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) + } + } + + if (newPermissions != permissions.RawValue) + { + permissions = new ChannelPermissions(newPermissions); + return true; + } + return false; + } + + public GuildUser GetUser(ulong id) + { + if (_users != null) + { + Member member; + if (_users.TryGetValue(id, out member)) + return member.User; + } + else + { + var user = _channel.Guild.GetUser(id); + if (_channel.Type == ChannelType.Text) + { + if (ResolvePermissions(user).ReadMessages) + return user; + } + else if (_channel.Type == ChannelType.Voice) + { + if (user.VoiceState?.VoiceChannel == _channel) + return user; + } + } + return null; + } + public IEnumerable GetUsers() + { + if (_users != null) + return _users.Select(x => x.Value.User); + else + { + var users = _channel.Guild.Users; + if (_channel.Type == ChannelType.Text) + { + var perms = new ChannelPermissions(); + return users.Where(x => ResolvePermissions(x, ref perms)); + } + else if (_channel.Type == ChannelType.Voice) + return users.Where(x => x.VoiceState?.VoiceChannel == _channel); + } + return Enumerable.Empty(); + } + + public void AddUser(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + var perms = new ChannelPermissions(); + ResolvePermissions(user, ref perms); + var member = new Member(user, ChannelPermissions.None); + _users[user.Id] = new Member(user, ChannelPermissions.None); + } + } + public void RemoveUser(ulong id) + { + Member ignored; + if (_users != null) + _users.TryRemove(id, out ignored); + } + } +} diff --git a/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs b/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs new file mode 100644 index 000000000..e14d5e210 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs @@ -0,0 +1,61 @@ +using System.Runtime.CompilerServices; + +namespace Discord +{ + internal static class PermissionsHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(uint allow, uint deny, PermissionBit bit) + { + if (HasBit(ref allow, (byte)bit)) + return PermValue.Allow; + else if (HasBit(ref deny, (byte)bit)) + return PermValue.Deny; + else + return PermValue.Inherit; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(uint value, PermissionBit bit) => HasBit(ref value, (byte)bit); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref uint rawValue, bool? value, PermissionBit bit) + { + if (value.HasValue) + { + if (value == true) + SetBit(ref rawValue, (byte)bit); + else + UnsetBit(ref rawValue, (byte)bit); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref uint allow, ref uint deny, PermValue? value, PermissionBit bit) + { + if (value.HasValue) + { + switch (value) + { + case PermValue.Allow: + SetBit(ref allow, (byte)bit); + UnsetBit(ref deny, (byte)bit); + break; + case PermValue.Deny: + UnsetBit(ref allow, (byte)bit); + SetBit(ref deny, (byte)bit); + break; + default: + UnsetBit(ref allow, (byte)bit); + UnsetBit(ref deny, (byte)bit); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasBit(ref uint value, byte bit) => (value & (1U << bit)) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetBit(ref uint value, byte bit) => value |= (1U << bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnsetBit(ref uint value, byte bit) => value &= ~(1U << bit); + } +} diff --git a/src/Discord.Net/Entities/IChannel.cs b/src/Discord.Net/Entities/IChannel.cs deleted file mode 100644 index f39678abb..000000000 --- a/src/Discord.Net/Entities/IChannel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace Discord -{ - public interface IChannel - { - /// Gets the unique identifier for this channel. - ulong Id { get; } - DiscordClient Client { get; } - - /// Gets the type of this channel. - ChannelType Type { get; } - bool IsText { get; } - bool IsVoice { get; } - bool IsPrivate { get; } - bool IsPublic { get; } - - /// Gets a collection of all users in this channel. - IEnumerable Users { get; } - } -} diff --git a/ref/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs similarity index 81% rename from ref/Entities/IEntity.cs rename to src/Discord.Net/Entities/IEntity.cs index ac707a69e..ecdde0a56 100644 --- a/ref/Entities/IEntity.cs +++ b/src/Discord.Net/Entities/IEntity.cs @@ -7,13 +7,12 @@ namespace Discord /// Gets the unique identifier for this object. TId Id { get; } } + public interface IEntity { /// Gets the DiscordClient that manages this object. DiscordClient Discord { get; } - /// Gets the state of this object. - EntityState State { get; } - + /// Downloads the latest values and updates this object. Task Update(); } diff --git a/ref/Entities/IMentionable.cs b/src/Discord.Net/Entities/IMentionable.cs similarity index 100% rename from ref/Entities/IMentionable.cs rename to src/Discord.Net/Entities/IMentionable.cs diff --git a/src/Discord.Net/Entities/IPrivateChannel.cs b/src/Discord.Net/Entities/IPrivateChannel.cs deleted file mode 100644 index 44d55a67f..000000000 --- a/src/Discord.Net/Entities/IPrivateChannel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IPrivateChannel : IChannel - { - User Recipient { get; } - } -} diff --git a/src/Discord.Net/Entities/IPublicChannel.cs b/src/Discord.Net/Entities/IPublicChannel.cs deleted file mode 100644 index 32b8c610e..000000000 --- a/src/Discord.Net/Entities/IPublicChannel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord -{ - public interface IPublicChannel : IChannel - { - } -} diff --git a/src/Discord.Net/Entities/ITextChannel.cs b/src/Discord.Net/Entities/ITextChannel.cs deleted file mode 100644 index af665ab0f..000000000 --- a/src/Discord.Net/Entities/ITextChannel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public interface ITextChannel : IChannel - { - Message GetMessage(ulong id); - Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before); - - Task SendMessage(string text, bool isTTS = false); - Task SendFile(string filePath); - Task SendFile(string filename, Stream stream); - - Task SendIsTyping(); - } -} diff --git a/src/Discord.Net/Entities/IVoiceChannel.cs b/src/Discord.Net/Entities/IVoiceChannel.cs deleted file mode 100644 index 7c3f2c194..000000000 --- a/src/Discord.Net/Entities/IVoiceChannel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord -{ - public interface IVoiceChannel : IChannel - { - } -} diff --git a/src/Discord.Net/Entities/Invite.cs b/src/Discord.Net/Entities/Invite.cs deleted file mode 100644 index a1b27ce4a..000000000 --- a/src/Discord.Net/Entities/Invite.cs +++ /dev/null @@ -1,151 +0,0 @@ -using APIInvite = Discord.API.Client.Invite; -using Discord.API.Client; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - public class Invite - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public class ServerInfo - { - /// Returns the unique identifier of this server. - public ulong Id { get; } - /// Returns the name of this server. - public string Name { get; } - - internal ServerInfo(ulong id, string name) - { - Id = id; - Name = name; - } - } - public class ChannelInfo - { - /// Returns the unique identifier of this channel. - public ulong Id { get; } - /// Returns the name of this channel. - public string Name { get; } - - internal ChannelInfo(ulong id, string name) - { - Id = id; - Name = name; - } - } - public class InviterInfo - { - /// Returns the unique identifier for this user. - public ulong Id { get; } - /// Returns the name of this user. - public string Name { get; } - /// Returns the by-name unique identifier for this user. - public ushort Discriminator { get; } - /// Returns the unique identifier for this user's avatar. - public string AvatarId { get; } - - /// Returns the full path to this user's avatar. - public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId); - - internal InviterInfo(ulong id, string name, ushort discriminator, string avatarId) - { - Id = id; - Name = name; - Discriminator = discriminator; - AvatarId = avatarId; - } - } - - public DiscordClient Client { get; } - - /// Gets the unique code for this invite. - public string Code { get; } - /// Gets, if enabled, an alternative human-readable invite code. - public string XkcdCode { get; } - - /// Gets information about the server this invite is attached to. - public ServerInfo Server { get; private set; } - /// Gets information about the channel this invite is attached to. - public ChannelInfo Channel { get; private set; } - /// Gets the time (in seconds) until the invite expires. - public int? MaxAge { get; private set; } - /// Gets the amount of times this invite has been used. - public int Uses { get; private set; } - /// Gets the max amount of times this invite may be used. - public int? MaxUses { get; private set; } - /// Returns true if this invite has expired, been destroyed, or you are banned from that server. - public bool IsRevoked { get; private set; } - /// If true, a user accepting this invite will be kicked from the server after closing their client. - public bool IsTemporary { get; private set; } - /// Gets when this invite was created. - public DateTime CreatedAt { get; private set; } - - /// Returns a URL for this invite using XkcdCode if available or Id if not. - public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; - - internal Invite(APIInvite model, DiscordClient client) - : this(model.Code, model.XkcdPass) - { - Client = client; - Update(model); - } - internal Invite(InviteReference model, DiscordClient client) - : this(model.Code, model.XkcdPass) - { - Client = client; - Update(model); - } - private Invite(string code, string xkcdCode) - { - Code = code; - XkcdCode = xkcdCode; - } - - internal void Update(APIInvite model) - { - Update(model as InviteReference); - - if (model.IsRevoked != null) - IsRevoked = model.IsRevoked.Value; - if (model.IsTemporary != null) - IsTemporary = model.IsTemporary.Value; - if (model.MaxAge != null) - MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null; - if (model.MaxUses != null) - MaxUses = model.MaxUses.Value; - if (model.Uses != null) - Uses = model.Uses.Value; - if (model.CreatedAt != null) - CreatedAt = model.CreatedAt.Value; - } - internal void Update(InviteReference model) - { - if (model.Guild != null) - Server = new ServerInfo(model.Guild.Id, model.Guild.Name); - if (model.Channel != null) - Channel = new ChannelInfo(model.Channel.Id, model.Channel.Name); - } - - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - public Task Accept() - => Client.ClientAPI.Send(new AcceptInviteRequest(Code)); - - internal Invite Clone() - { - var result = new Invite(Code, XkcdCode); - _cloner(this, result); - return result; - } - - public override string ToString() => $"{Server}/{XkcdCode ?? Code}"; - } -} diff --git a/src/Discord.Net/Entities/Invites/GuildInvite.cs b/src/Discord.Net/Entities/Invites/GuildInvite.cs new file mode 100644 index 000000000..d4387c744 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/GuildInvite.cs @@ -0,0 +1,71 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.InviteMetadata; + +namespace Discord +{ + public class GuildInvite : IInvite, IEntity + { + /// + public string Code { get; } + /// Gets the channel this invite is attached to. + public GuildChannel Channel { get; } + + /// + public string XkcdCode { get; private set; } + /// Gets the time (in seconds) until the invite expires, or null if it never expires. + public int? MaxAge { get; private set; } + /// Gets the amount of times this invite has been used. + public int Uses { get; private set; } + /// Gets the max amount of times this invite may be used, or null if there is no limit. + public int? MaxUses { get; private set; } + /// Returns true if this invite has expired or been deleted. + public bool IsRevoked { get; private set; } + /// Returns true if a user accepting this invite will be kicked from the guild after closing their client. + public bool IsTempMembership { get; private set; } + + /// Gets the guild this invite is attached to. + public Guild Guild => Channel.Guild; + /// + public DiscordClient Discord => Guild.Discord; + /// + public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; + /// + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; + /// + string IEntity.Id => Code; + /// + InviteChannel IInvite.Channel => new InviteChannel(Channel.Id, Channel.Name); + /// + InviteGuild IInvite.Guild => new InviteGuild(Guild.Id, Guild.Name); + + internal GuildInvite(string code, GuildChannel channel) + { + Code = code; + Channel = channel; + } + + internal void Update(Model model) + { + XkcdCode = model.XkcdPass; + IsRevoked = model.Revoked; + IsTempMembership = model.Temporary; + MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; + MaxUses = model.MaxUses; + Uses = model.Uses; + } + + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + /// Deletes this invite. + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + } +} diff --git a/src/Discord.Net/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs new file mode 100644 index 000000000..8765cdc93 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/IInvite.cs @@ -0,0 +1,20 @@ +namespace Discord +{ + public interface IInvite : IEntity + { + /// Gets the unique code for this invite. + string Code { get; } + /// Gets, if enabled, an alternative human-readable invite code. + string XkcdCode { get; } + + /// Returns a URL for this invite using Code. + string Url { get; } + /// Returns a URL for this invite using XkcdCode if available or null if not. + string XkcdUrl { get; } + + /// Gets information about the guild this invite is attached to. + InviteGuild Guild { get; } + /// Gets information about the channel this invite is attached to. + InviteChannel Channel { get; } + } +} diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs new file mode 100644 index 000000000..2dbd07ec5 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -0,0 +1,48 @@ +using Discord.API.Rest; +using System.Threading.Tasks; +using Model = Discord.API.Invite; + +namespace Discord +{ + public class PublicInvite : IInvite, IEntity + { + /// + public string Code { get; } + /// + string IEntity.Id => Code; + /// + public DiscordClient Discord { get; } + + /// + public InviteGuild Guild { get; private set; } + /// + public InviteChannel Channel { get; private set; } + /// + public string XkcdCode { get; private set; } + + /// + public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; + /// + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; + + internal PublicInvite(string code, DiscordClient client) + { + Code = code; + Discord = client; + } + + internal void Update(Model model) + { + XkcdCode = model.XkcdPass; + Guild = new InviteGuild(model.Guild.Id, model.Guild.Name); + Channel = new InviteChannel(model.Channel.Id, model.Channel.Name); + } + + /// + public async Task Update() + => Update(await Discord.RestClient.Send(new GetInviteRequest(Code)).ConfigureAwait(false)); + + /// + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net/Entities/Invites/InviteChannel.cs b/src/Discord.Net/Entities/Invites/InviteChannel.cs new file mode 100644 index 000000000..45ac5b084 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/InviteChannel.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public struct InviteChannel + { + /// Returns the unique identifier for this channel. + public ulong Id { get; } + /// Returns the name of this channel. + public string Name { get; } + + internal InviteChannel(ulong id, string name) + { + Id = id; + Name = name; + } + } +} diff --git a/src/Discord.Net/Entities/Invites/InviteGuild.cs b/src/Discord.Net/Entities/Invites/InviteGuild.cs new file mode 100644 index 000000000..0354bd282 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/InviteGuild.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public struct InviteGuild + { + /// Returns the unique identifier for this guild. + public ulong Id { get; } + /// Returns the name of this guild. + public string Name { get; } + + internal InviteGuild(ulong id, string name) + { + Id = id; + Name = name; + } + } +} diff --git a/src/Discord.Net/Entities/Managers/MessageManager.cs b/src/Discord.Net/Entities/Managers/MessageManager.cs deleted file mode 100644 index 14b569750..000000000 --- a/src/Discord.Net/Entities/Managers/MessageManager.cs +++ /dev/null @@ -1,146 +0,0 @@ -using APIMessage = Discord.API.Client.Message; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal class MessageManager : IEnumerable - { - private readonly ITextChannel _channel; - private readonly int _size; - private readonly ConcurrentDictionary _messages; - private readonly ConcurrentQueue _orderedMessages; - - public MessageManager(ITextChannel channel, int size = 0) - { - _channel = channel; - _size = size; - if (size > 0) - { - _messages = new ConcurrentDictionary(2, size); - _orderedMessages = new ConcurrentQueue(); - } - } - - internal Message Add(APIMessage model, User user) => Add(new Message(model, _channel, user)); - internal Message Add(ulong id, User user) => Add(new Message(id, _channel, user)); - private Message Add(Message message) - { - message.State = MessageState.Normal; - if (_size > 0) - { - if (_messages.TryAdd(message.Id, message)) - { - _orderedMessages.Enqueue(message.Id); - - ulong msgId; - while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) - { - Message msg; - if (_messages.TryRemove(msgId, out msg)) - msg.State = MessageState.Detached; - } - } - } - return message; - } - internal Message Remove(ulong id) - { - if (_size > 0) - { - Message msg; - if (_messages.TryRemove(id, out msg)) - return msg; - } - return new Message(id, _channel, null) { State = MessageState.Deleted }; - } - - public Message Get(ulong id, ulong? userId = null) - { - if (_messages != null) - { - Message result; - if (_messages.TryGetValue(id, out result)) - return result; - } - return new Message(id, _channel, userId != null ? (_channel as Channel).GetUser(userId.Value) : null) { State = MessageState.Detached }; - } - - public async Task Download(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - { - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return new Message[0]; - - try - { - var request = new GetMessagesRequest(_channel.Id) - { - Limit = limit, - RelativeDir = relativeMessageId.HasValue ? relativeDir == Relative.Before ? "before" : "after" : null, - RelativeId = relativeMessageId ?? 0 - }; - var msgs = await _channel.Client.ClientAPI.Send(request).ConfigureAwait(false); - var server = (_channel as PublicChannel)?.Server; - - return msgs.Select(x => - { - Message msg = null; - ulong id = x.Author.Id; - var user = server?.GetUser(id) ?? (_channel as Channel).GetUser(id); - msg = new Message(x, _channel, user); - return msg; - }).ToArray(); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - return new Message[0]; - } - } - - public Task Send(string text, bool isTTS) - { - if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); - if (text.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); - return Task.FromResult(_channel.Client.MessageQueue.QueueSend(_channel, text, isTTS)); - } - public async Task SendFile(string filePath) - { - using (var stream = File.OpenRead(filePath)) - return await SendFile(Path.GetFileName(filePath), stream).ConfigureAwait(false); - } - public async Task SendFile(string filename, Stream stream) - { - var request = new SendFileRequest(_channel.Id) - { - Filename = filename, - Stream = stream - }; - var response = await _channel.Client.ClientAPI.Send(request).ConfigureAwait(false); - - return Add(response, (_channel as Channel).CurrentUser); - } - - public IEnumerator GetEnumerator() - { - return _orderedMessages - .Select(x => - { - Message msg; - if (_messages.TryGetValue(x, out msg)) - return msg; - return null; - }) - .Where(x => x != null).GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Discord.Net/Entities/Managers/PermissionManager.cs b/src/Discord.Net/Entities/Managers/PermissionManager.cs deleted file mode 100644 index 4b8eb5c8c..000000000 --- a/src/Discord.Net/Entities/Managers/PermissionManager.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using APIChannel = Discord.API.Client.Channel; - -namespace Discord -{ - internal class PermissionManager - { - public struct Member - { - public User User { get; } - public ChannelPermissions Permissions { get; } - - public Member(User user, ChannelPermissions permissions) - { - User = user; - Permissions = permissions; - } - } - - private readonly PublicChannel _channel; - private readonly ConcurrentDictionary _users; - private Dictionary _rules; - - public IEnumerable Users => _users.Select(x => x.Value); - public IEnumerable Rules => _rules.Values; - - public PermissionManager(PublicChannel channel, APIChannel model, int initialSize = -1) - { - _channel = channel; - if (initialSize >= 0) - _users = new ConcurrentDictionary(2, initialSize); - Update(model); - } - - public void Update(APIChannel model) - { - _rules = model.PermissionOverwrites - .Select(x => new Channel.PermissionRule(EnumConverters.ToPermissionTarget(x.Type), x.Id, x.Allow, x.Deny)) - .ToDictionary(x => x.TargetId); - UpdatePermissions(); - } - - public ChannelTriStatePermissions? GetOverwrite(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - Channel.PermissionRule rule; - if (_rules.TryGetValue(user.Id, out rule)) - return rule.Permissions; - return null; - } - public ChannelTriStatePermissions? GetOverwrite(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - - Channel.PermissionRule rule; - if (_rules.TryGetValue(role.Id, out rule)) - return rule.Permissions; - return null; - } - public Task AddOrUpdateOverwrite(User user, ChannelTriStatePermissions permissions) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - return AddOrUpdateOverwrite(user.Id, PermissionTarget.User, permissions); - } - public Task AddOrUpdateOverwrite(Role role, ChannelTriStatePermissions permissions) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - return AddOrUpdateOverwrite(role.Id, PermissionTarget.Role, permissions); - } - private Task AddOrUpdateOverwrite(ulong id, PermissionTarget type, ChannelTriStatePermissions permissions) - { - var request = new AddOrUpdateChannelPermissionsRequest(id) - { - TargetId = id, - TargetType = EnumConverters.ToString(type), - Allow = permissions.AllowValue, - Deny = permissions.DenyValue - }; - return _channel.Client.ClientAPI.Send(request); - } - public Task RemoveOverwrite(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - return RemoveOverwrite(user.Id); - } - public Task RemoveOverwrite(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - return RemoveOverwrite(role.Id); - } - private async Task RemoveOverwrite(ulong id) - { - try { await _channel.Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(_channel.Id, id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - public ChannelPermissions GetPermissions(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - return member.Permissions; - else - return ChannelPermissions.None; - } - else - { - ChannelPermissions perms = new ChannelPermissions(); - ResolvePermissions(user, ref perms); - return perms; - } - } - public void UpdatePermissions() - { - if (_users != null) - { - foreach (var pair in _users) - { - var member = pair.Value; - var perms = member.Permissions; - if (ResolvePermissions(member.User, ref perms)) - _users[pair.Key] = new Member(member.User, perms); - } - } - } - public void UpdatePermissions(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - { - var perms = member.Permissions; - if (ResolvePermissions(member.User, ref perms)) - _users[user.Id] = new Member(member.User, perms); - } - } - } - public bool ResolvePermissions(User user, ref ChannelPermissions permissions) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - uint newPermissions = 0; - var server = user.Server; - - var mask = ChannelPermissions.All(_channel.Type).RawValue; - if (_channel.IsPrivate || user.IsOwner) - newPermissions = mask; //Private messages and owners always have all permissions - else - { - //Start with this user's server permissions - newPermissions = server.GetPermissions(user).RawValue; - var rules = _rules; - - Channel.PermissionRule rule; - var roles = user.Roles.ToArray(); - if (roles.Length > 0) - { - for (int i = 0; i < roles.Length; i++) - { - if (rules.TryGetValue(roles[i].Id, out rule)) - newPermissions &= ~rule.Permissions.DenyValue; - } - for (int i = 0; i < roles.Length; i++) - { - if (rules.TryGetValue(roles[i].Id, out rule)) - newPermissions |= rule.Permissions.AllowValue; - } - } - if (rules.TryGetValue(user.Id, out rule)) - newPermissions = (newPermissions & ~rule.Permissions.DenyValue) | rule.Permissions.AllowValue; - - if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) - newPermissions = mask; //ManageRolesOrPermissions gives all permisions - else if (_channel.IsText && !newPermissions.HasBit((byte)PermissionBits.ReadMessages)) - newPermissions = 0; //No read permission on a text channel removes all other permissions - else if (_channel.IsVoice && !newPermissions.HasBit((byte)PermissionBits.Connect)) - newPermissions = 0; //No connect permissions on a voice channel removes all other permissions - else - newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) - } - - if (newPermissions != permissions.RawValue) - { - permissions = new ChannelPermissions(newPermissions); - return true; - } - return false; - } - - public void AddUser(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - var perms = new ChannelPermissions(); - ResolvePermissions(user, ref perms); - var member = new Member(user, ChannelPermissions.None); - _users[user.Id] = new Member(user, ChannelPermissions.None); - } - } - public void RemoveUser(ulong id) - { - Member ignored; - if (_users != null) - _users.TryRemove(id, out ignored); - } - } -} diff --git a/src/Discord.Net/Entities/Message.cs b/src/Discord.Net/Entities/Message.cs index a812cd897..7e255ddf7 100644 --- a/src/Discord.Net/Entities/Message.cs +++ b/src/Discord.Net/Entities/Message.cs @@ -1,327 +1,116 @@ -using System; +using Discord.API.Rest; +using Discord.Net; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Collections.Immutable; +using System.Net; using System.Threading.Tasks; -using APIMessage = Discord.API.Client.Message; +using Model = Discord.API.Message; namespace Discord { - public class Message + public class Message : IEntity { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>"); - private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>"); - private static readonly Regex _roleRegex = new Regex(@"@everyone"); - private static readonly Attachment[] _initialAttachments = new Attachment[0]; - private static readonly Embed[] _initialEmbeds = new Embed[0]; - - internal static string CleanUserMentions(PublicChannel channel, string text, List users = null) - { - return _userRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id)) - { - var user = channel.GetUser(id); - if (user != null) - { - if (users != null) - users.Add(user); - return '@' + user.Name; - } - } - return e.Value; //User not found or parse failed - })); - } - internal static string CleanChannelMentions(PublicChannel channel, string text, List channels = null) - { - var server = channel.Server; - if (server == null) return text; - - return _channelRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id)) - { - var mentionedChannel = server.GetChannel(id); - if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) - { - if (channels != null) - channels.Add(mentionedChannel); - return '#' + mentionedChannel.Name; - } - } - return e.Value; //Channel not found or parse failed - })); - } - /*internal static string CleanRoleMentions(User user, Channel channel, string text, List roles = null) - { - var server = channel.Server; - if (server == null) return text; - - return _roleRegex.Replace(text, new MatchEvaluator(e => - { - if (roles != null && user.GetPermissions(channel).MentionEveryone) - roles.Add(server.EveryoneRole); - return e.Value; - })); - }*/ - - internal static string ResolveMentions(IChannel channel, string text) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (text == null) throw new ArgumentNullException(nameof(text)); - - var publicChannel = channel as PublicChannel; - if (publicChannel != null) - { - text = CleanUserMentions(publicChannel, text); - text = CleanChannelMentions(publicChannel, text); - //text = CleanRoleMentions(publicChannel, text); - } - return text; - } - - public class Attachment : File - { - /// Unique identifier for this file. - public string Id { get; internal set; } - /// Size, in bytes, of this file file. - public int Size { get; internal set; } - /// Filename of this file. - public string Filename { get; internal set; } - - internal Attachment() { } - } - - public class Embed - { - /// URL of this embed. - public string Url { get; internal set; } - /// Type of this embed. - public string Type { get; internal set; } - /// Title for this embed. - public string Title { get; internal set; } - /// Summary of this embed. - public string Description { get; internal set; } - /// Returns information about the author of this embed. - public EmbedLink Author { get; internal set; } - /// Returns information about the providing website of this embed. - public EmbedLink Provider { get; internal set; } - /// Returns the thumbnail of this embed. - public File Thumbnail { get; internal set; } - /// Returns the video information of this embed. - public File Video { get; internal set; } - - internal Embed() { } - } - - public class EmbedLink - { - /// URL of this embed provider. - public string Url { get; internal set; } - /// Name of this embed provider. - public string Name { get; internal set; } - - internal EmbedLink() { } - } - - public class File - { - /// Download url for this file. - public string Url { get; internal set; } - /// Preview url for this file. - public string ProxyUrl { get; internal set; } - /// Width of this file, if it is an image. - public int? Width { get; internal set; } - /// Height of this file, if it is an image. - public int? Height { get; internal set; } - - internal File() { } - } - - public DiscordClient Client => Channel.Client; - - /// Returns the unique identifier for this message. - public ulong Id { get; internal set; } - /// Returns the channel this message was sent to. - public ITextChannel Channel { get; } - /// Returns the author of this message. + /// + public ulong Id { get; } + public IMessageChannel Channel { get; } public User User { get; } - /// Returns true if the message was sent as text-to-speech by someone with permissions to do so. public bool IsTTS { get; internal set; } - /// Returns the state of this message. Only useful if UseMessageQueue is true. - public MessageState State { get; internal set; } - /// Returns the raw content of this message as it was received from the server. - public string RawText { get; internal set; } - /// Returns the content of this message with any special references such as mentions converted. - public string Text { get; internal set; } - /// Returns the timestamp for when this message was sent. - public DateTime Timestamp { get; private set; } - /// Returns the timestamp for when this message was last edited. - public DateTime? EditedTimestamp { get; private set; } - /// Returns the attachments included in this message. - public Attachment[] Attachments { get; private set; } - /// Returns a collection of all embeded content in this message. - public Embed[] Embeds { get; private set; } - - /// Returns a collection of all users mentioned in this message. - public IEnumerable MentionedUsers { get; internal set; } - /// Returns a collection of all channels mentioned in this message. - public IEnumerable MentionedChannels { get; internal set; } - /// Returns a collection of all roles mentioned in this message. - public IEnumerable MentionedRoles { get; internal set; } - + public string RawText { get; internal set; } + public string Text { get; internal set; } + public DateTime Timestamp { get; internal set; } + public DateTime? EditedTimestamp { get; private set; } + public IReadOnlyList Attachments { get; private set; } + public IReadOnlyList Embeds { get; private set; } + public IReadOnlyList MentionedUsers { get; private set; } + public IReadOnlyList MentionedChannels { get; private set; } + public IReadOnlyList MentionedRoles { get; private set; } internal int Nonce { get; set; } - /// Returns the server containing the channel this message was sent to. - public Server Server => (Channel as PublicChannel)?.Server; - /// Returns if this message was sent from the logged-in accounts. - public bool IsAuthor => User != null && User.Id == Client.CurrentUser?.Id; + public DiscordClient Discord => Channel.Discord; + public bool IsAuthor => false; - internal Message(APIMessage model, ITextChannel channel, User user) - : this(model.Id, channel, user) - { - Update(model); - } - internal Message(ulong id, ITextChannel channel, User user) + internal Message(ulong id, IMessageChannel channel, User user) { Id = id; Channel = channel; User = user; - Attachments = _initialAttachments; - Embeds = _initialEmbeds; } - internal void Update(APIMessage model) - { - var channel = Channel; - if (model.Attachments != null) - { - Attachments = model.Attachments - .Select(x => new Attachment() - { - Id = x.Id, - Url = x.Url, - ProxyUrl = x.ProxyUrl, - Width = x.Width, - Height = x.Height, - Size = x.Size, - Filename = x.Filename - }) - .ToArray(); - } - if (model.Embeds != null) - { - Embeds = model.Embeds.Select(x => - { - EmbedLink author = null, provider = null; - File thumbnail = null, video = null; + internal void Update(Model model) + { + var channel = Channel; + bool isPublic = channel.Type != ChannelType.DM; - if (x.Author != null) - author = new EmbedLink { Url = x.Author.Url, Name = x.Author.Name }; - if (x.Provider != null) - provider = new EmbedLink { Url = x.Provider.Url, Name = x.Provider.Name }; - if (x.Thumbnail != null) - thumbnail = new File { Url = x.Thumbnail.Url, ProxyUrl = x.Thumbnail.ProxyUrl, Width = x.Thumbnail.Width, Height = x.Thumbnail.Height }; - if (x.Video != null) - video = new File { Url = x.Video.Url, ProxyUrl = null, Width = x.Video.Width, Height = x.Video.Height }; + IsTTS = model.IsTextToSpeech; + Timestamp = model.Timestamp; + EditedTimestamp = model.EditedTimestamp; + RawText = model.Content; - return new Embed - { - Url = x.Url, - Type = x.Type, - Title = x.Title, - Description = x.Description, - Author = author, - Provider = provider, - Thumbnail = thumbnail, - Video = video - }; - }).ToArray(); - } - - if (model.IsTextToSpeech != null) - IsTTS = model.IsTextToSpeech.Value; - if (model.Timestamp != null) - Timestamp = model.Timestamp.Value; - if (model.EditedTimestamp != null) - EditedTimestamp = model.EditedTimestamp; - if (model.Mentions != null) - { - MentionedUsers = model.Mentions - .Select(x => (Channel as Channel).GetUser(x.Id)) - .Where(x => x != null) - .ToArray(); - } - if (model.IsMentioningEveryone != null) + if (model.Attachments.Length > 0) { - var server = (channel as PublicChannel).Server; - if (model.IsMentioningEveryone.Value && server != null) - MentionedRoles = new Role[] { server.EveryoneRole }; - else - MentionedRoles = Enumerable.Empty(); + var attachments = new Attachment[model.Attachments.Length]; + for (int i = 0; i < attachments.Length; i++) + attachments[i] = new Attachment(model.Attachments[i]); + Attachments = ImmutableArray.Create(attachments); } - if (model.Content != null) - { - string text = model.Content; - RawText = text; - - List mentionedChannels = null; - if (Channel.IsPublic) - mentionedChannels = new List(); - - text = CleanUserMentions(Channel as PublicChannel, text); - text = CleanChannelMentions(Channel as PublicChannel, text, mentionedChannels); - - if (Channel.IsPublic) - MentionedChannels = mentionedChannels; - - Text = text; - } - } + else + Attachments = ImmutableArray.Empty; - public Task Edit(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); + if (model.Embeds.Length > 0) + { + var embeds = new Embed[model.Attachments.Length]; + for (int i = 0; i < embeds.Length; i++) + embeds[i] = new Embed(model.Embeds[i]); + Embeds = ImmutableArray.Create(embeds); + } + else + Embeds = ImmutableArray.Empty; - var channel = Channel; + if (model.Mentions.Length > 0) + { + var users = new GuildUser[model.Mentions.Length]; + int j = 0; + for (int i = 0; i < users.Length; i++) + { + var user = Channel.GetUser(model.Mentions[i].Id) as GuildUser; + if (user != null) + users[j++] = user; + } + MentionedUsers = ImmutableArray.Create(users, 0, j); + } + else + MentionedUsers = ImmutableArray.Empty; - if (text.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); - - Client.MessageQueue.QueueEdit(this, text); - return TaskHelper.CompletedTask; - } - public Task Delete() - { - Client.MessageQueue.QueueDelete(this); - return TaskHelper.CompletedTask; - } + if (model.IsMentioningEveryone && isPublic) + MentionedRoles = ImmutableArray.Create((channel as GuildChannel).Guild.EveryoneRole); + else + MentionedRoles = ImmutableArray.Empty; - /// Returns true if the logged-in user was mentioned. - public bool IsMentioningMe(bool includeRoles = false) - { - User me = Server != null ? Server.CurrentUser : Channel.Client.PrivateUser; - if (includeRoles) + string text = model.Content; + if (isPublic) { - return (MentionedUsers?.Contains(me) ?? false) || - (MentionedRoles?.Any(x => me.HasRole(x)) ?? false); + var publicChannel = channel as GuildChannel; + var mentionedChannels = ImmutableArray.CreateBuilder(); + text = MentionHelper.CleanUserMentions(publicChannel, text); + text = MentionHelper.CleanChannelMentions(publicChannel, text, mentionedChannels); + MentionedChannels = mentionedChannels.ToImmutable(); } else - return MentionedUsers?.Contains(me) ?? false; + MentionedChannels = ImmutableArray.Empty; + Text = text; } - internal Message Clone() + public bool IsMentioningMe(bool includeRoles = false) => false; + + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + /// Deletes this message. + public async Task Delete() { - var result = new Message(Id, Channel, User); - _cloner(this, result); - return result; + try { await Discord.RestClient.Send(new DeleteMessageRequest(Channel.Id, Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - - public override string ToString() => $"{User}: {RawText}"; - } + } } diff --git a/src/Discord.Net/Entities/Permissions.cs b/src/Discord.Net/Entities/Permissions.cs deleted file mode 100644 index badccc116..000000000 --- a/src/Discord.Net/Entities/Permissions.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Discord -{ - public struct ServerPermissions - { - public static ServerPermissions None { get; } = new ServerPermissions(); - public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); - - public uint RawValue { get; } - - /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite); - /// If True, a user may ban users from the server. - public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.BanMembers); - /// If True, a user may kick users from the server. - public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.KickMembers); - /// If True, a user may adjust roles. This also implictly grants all other permissions. - public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify channels. - public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); - /// If True, a user may adjust server properties. - public bool ManageServer => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageServer); - - /// If True, a user may join channels. - public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBits.Speak); - /// If True, a user may mute users. - public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); - - public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this(new ServerPermissions(), createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageServer, readMessages, - sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation) - { - } - public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - { - uint value = basePerms.RawValue; - - PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref value, banMembers, PermissionBits.BanMembers); - PermissionsHelper.SetValue(ref value, kickMembers, PermissionBits.KickMembers); - PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref value, manageServer, PermissionBits.ManageServer); - PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref value, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref value, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref value, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref value, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref value, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref value, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); - - RawValue = value; - } - public ServerPermissions(uint rawValue) { RawValue = rawValue; } - - public override string ToString() => Convert.ToString(RawValue, 2); - } - - public struct ChannelPermissions - { - public static ChannelPermissions None { get; } = new ChannelPermissions(); - public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2)); - public static ChannelPermissions PrivateOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2)); - public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2)); - public static ChannelPermissions All(ChannelType channelType) - { - switch (channelType) - { - case ChannelType.Text: return TextOnly; - case ChannelType.Voice: return VoiceOnly; - case ChannelType.Private: return PrivateOnly; - default: return None; - } - } - - public uint RawValue { get; } - - /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite); - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify this channel. - public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); - - /// If True, a user may join channels. - public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBits.Speak); - /// If True, a user may mute users. - public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); - - public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this(new ChannelPermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, - manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) - { - } - public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - { - uint value = basePerms.RawValue; - - PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref value, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref value, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref value, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref value, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref value, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref value, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); - - RawValue = value; - } - public ChannelPermissions(uint rawValue) { RawValue = rawValue; } - - public override string ToString() => Convert.ToString(RawValue, 2); - } - - public struct ChannelTriStatePermissions - { - public static ChannelTriStatePermissions InheritAll { get; } = new ChannelTriStatePermissions(); - - public uint AllowValue { get; } - public uint DenyValue { get; } - - /// If True, a user may create invites. - public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.CreateInstantInvite); - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify this channel. - public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageChannel); - /// If True, a user may join channels. - public PermValue ReadMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public PermValue SendMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public PermValue SendTTSMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public PermValue ManageMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public PermValue EmbedLinks => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public PermValue AttachFiles => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public PermValue ReadMessageHistory => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Speak); - /// If True, a user may mute users. - public PermValue MuteMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public PermValue DeafenMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public PermValue MoveMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.UseVoiceActivation); - - public ChannelTriStatePermissions(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - : this(new ChannelTriStatePermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, - manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) - { - } - public ChannelTriStatePermissions(ChannelTriStatePermissions basePerms, PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - { - uint allow = basePerms.AllowValue, deny = basePerms.DenyValue; - - PermissionsHelper.SetValue(ref allow, ref deny, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref allow, ref deny, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref allow, ref deny, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref allow, ref deny, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref allow, ref deny, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref allow, ref deny, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref allow, ref deny, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref allow, ref deny, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref allow, ref deny, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref allow, ref deny, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref allow, ref deny, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref allow, ref deny, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref allow, ref deny, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref allow, ref deny, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref allow, ref deny, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref allow, ref deny, useVoiceActivation, PermissionBits.UseVoiceActivation); - - AllowValue = allow; - DenyValue = deny; - } - public ChannelTriStatePermissions(uint allow = 0, uint deny = 0) - { - AllowValue = allow; - DenyValue = deny; - } - - public override string ToString() => $"Allow: {Convert.ToString(AllowValue, 2)}, Deny: {Convert.ToString(DenyValue, 2)}"; - } - internal static class PermissionsHelper - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(uint allow, uint deny, PermissionBits bit) - { - if (allow.HasBit((byte)bit)) - return PermValue.Allow; - else if (deny.HasBit((byte)bit)) - return PermValue.Deny; - else - return PermValue.Inherit; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(uint value, PermissionBits bit) => value.HasBit((byte)bit); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint rawValue, bool? value, PermissionBits bit) - { - if (value.HasValue) - { - if (value == true) - SetBit(ref rawValue, bit); - else - UnsetBit(ref rawValue, bit); - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint allow, ref uint deny, PermValue? value, PermissionBits bit) - { - if (value.HasValue) - { - switch (value) - { - case PermValue.Allow: - SetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - case PermValue.Deny: - UnsetBit(ref allow, bit); - SetBit(ref deny, bit); - break; - default: - UnsetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBit(ref uint value, PermissionBits bit) => value |= 1U << (int)bit; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void UnsetBit(ref uint value, PermissionBits bit) => value &= ~(1U << (int)bit); - } -} diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs new file mode 100644 index 000000000..7b37fccd0 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -0,0 +1,122 @@ +using System; + +namespace Discord +{ + public struct ChannelPermissions + { + private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2)); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2)); + private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2)); + + /// Gets a blank ChannelPermissions that grants no permissions. + public static ChannelPermissions None { get; } = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for a given channelType. + public static ChannelPermissions All(ChannelType channelType) + { + switch (channelType) + { + case ChannelType.DM: + return _allText; + case ChannelType.Text: + return _allDM; + case ChannelType.Voice: + return _allVoice; + default: + throw new ArgumentOutOfRangeException(nameof(channelType)); + } + } + + /// Gets a packed value representing all the permissions in this ChannelPermissions. + public uint RawValue { get; } + + /// If True, a user may create invites. + public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBit.CreateInstantInvite); + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify this channel. + public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageChannel); + + /// If True, a user may join channels. + public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBit.Speak); + /// If True, a user may mute users. + public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBit.UseVoiceActivation); + + /// Creates a new ChannelPermissions with the provided packed value. + public ChannelPermissions(uint rawValue) { RawValue = rawValue; } + + private ChannelPermissions(uint initialValue, bool? createInstantInvite = null, bool? managePermissions = null, + bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, + bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, + bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + { + uint value = initialValue; + + PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref value, managePermissions, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref value, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref value, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref value, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref value, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref value, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref value, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref value, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref value, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref value, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref value, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBit.UseVoiceActivation); + + RawValue = value; + } + + /// Creates a new ChannelPermissions with the provided permissions. + public ChannelPermissions(bool createInstantInvite = false, bool managePermissions = false, + bool manageChannel = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, + bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, + bool mentionEveryone = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, + bool moveMembers = false, bool useVoiceActivation = false) + : this(0, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) { } + + /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? managePermissions = null, + bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, + bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, + bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + => new ChannelPermissions(RawValue, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation); + + /// + public override string ToString() => Convert.ToString(RawValue, 2); + } +} diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs new file mode 100644 index 000000000..7be0febe1 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -0,0 +1,119 @@ +using System; + +namespace Discord +{ + public struct GuildPermissions + { + /// Gets a blank GuildPermissions that grants no permissions. + public static GuildPermissions None { get; } = new GuildPermissions(); + /// Gets a GuildPermissions that grants all permissions. + public static GuildPermissions All { get; } = new GuildPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); + + /// Gets a packed value representing all the permissions in this GuildPermissions. + public uint RawValue { get; } + + /// If True, a user may create invites. + public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBit.CreateInstantInvite); + /// If True, a user may ban users from the guild. + public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.BanMembers); + /// If True, a user may kick users from the guild. + public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.KickMembers); + /// If True, a user may adjust roles. This also implictly grants all other permissions. + public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify channels. + public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageChannel); + /// If True, a user may adjust guild properties. + public bool ManageGuild => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageGuild); + + /// If True, a user may join channels. + public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBit.Speak); + /// If True, a user may mute users. + public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBit.UseVoiceActivation); + + /// Creates a new GuildPermissions with the provided packed value. + public GuildPermissions(uint rawValue) { RawValue = rawValue; } + + private GuildPermissions(uint initialValue, bool? createInstantInvite = null, bool? manageRoles = null, + bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageGuild = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + { + uint value = initialValue; + + PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref value, banMembers, PermissionBit.BanMembers); + PermissionsHelper.SetValue(ref value, kickMembers, PermissionBit.KickMembers); + PermissionsHelper.SetValue(ref value, manageRoles, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref value, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref value, manageGuild, PermissionBit.ManageGuild); + PermissionsHelper.SetValue(ref value, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref value, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref value, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref value, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref value, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref value, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref value, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref value, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref value, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBit.UseVoiceActivation); + + RawValue = value; + } + + /// Creates a new GuildPermissions with the provided permissions. + public GuildPermissions(bool createInstantInvite = false, bool manageRoles = false, + bool kickMembers = false, bool banMembers = false, bool manageChannel = false, bool manageGuild = false, + bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, + bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, + bool moveMembers = false, bool useVoiceActivation = false) + : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageGuild, readMessages, + sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation) { } + + /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. + public GuildPermissions Modify(bool? createInstantInvite = null, bool? manageRoles = null, + bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageGuild = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageGuild, readMessages, + sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation); + + /// + public override string ToString() => Convert.ToString(RawValue, 2); + } +} diff --git a/src/Discord.Net/Entities/Permissions/Overwrite.cs b/src/Discord.Net/Entities/Permissions/Overwrite.cs new file mode 100644 index 000000000..d964e7068 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/Overwrite.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.Overwrite; + +namespace Discord +{ + public struct Overwrite + { + /// Gets the unique identifier for the object this overwrite is targeting. + public ulong TargetId { get; } + /// Gets the type of object this overwrite is targeting. + public PermissionTarget TargetType { get; } + /// Gets the permissions associated with this overwrite entry. + public OverwritePermissions Permissions { get; } + + /// Creates a new Overwrite with provided target information and modified permissions. + internal Overwrite(Model model) + { + TargetId = model.TargetId; + TargetType = model.TargetType; + Permissions = new OverwritePermissions(model.Allow, model.Deny); + } + } +} diff --git a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs new file mode 100644 index 000000000..0a569c264 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs @@ -0,0 +1,113 @@ +using System; + +namespace Discord +{ + public struct OverwritePermissions + { + /// Gets a blank OverwritePermissions that inherits all permissions. + public static OverwritePermissions InheritAll { get; } = new OverwritePermissions(); + /// Gets a OverwritePermissions that grants all permissions for a given channelType. + public static OverwritePermissions AllowAll(ChannelType channelType) + => new OverwritePermissions(ChannelPermissions.All(channelType).RawValue, 0); + /// Gets a OverwritePermissions that denies all permissions for a given channelType. + public static OverwritePermissions DenyAll(ChannelType channelType) + => new OverwritePermissions(0, ChannelPermissions.All(channelType).RawValue); + + /// Gets a packed value representing all the allowed permissions in this OverwritePermissions. + public uint AllowValue { get; } + /// Gets a packed value representing all the denied permissions in this OverwritePermissions. + public uint DenyValue { get; } + + /// If True, a user may create invites. + public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.CreateInstantInvite); + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify this channel. + public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageChannel); + /// If True, a user may join channels. + public PermValue ReadMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public PermValue SendMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public PermValue SendTTSMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public PermValue ManageMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public PermValue EmbedLinks => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public PermValue AttachFiles => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public PermValue ReadMessageHistory => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.Speak); + /// If True, a user may mute users. + public PermValue MuteMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public PermValue DeafenMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public PermValue MoveMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.UseVoiceActivation); + + /// Creates a new OverwritePermissions with the provided allow and deny packed values. + public OverwritePermissions(uint allowValue, uint denyValue) + { + AllowValue = allowValue; + DenyValue = denyValue; + } + + private OverwritePermissions(uint allowValue, uint denyValue, PermValue? createInstantInvite = null, PermValue? managePermissions = null, + PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, + PermValue? moveMembers = null, PermValue? useVoiceActivation = null) + { + PermissionsHelper.SetValue(ref allowValue, ref denyValue, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, managePermissions, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, useVoiceActivation, PermissionBit.UseVoiceActivation); + + AllowValue = allowValue; + DenyValue = denyValue; + } + + /// Creates a new ChannelPermissions with the provided permissions. + public OverwritePermissions(PermValue createInstantInvite = PermValue.Inherit, PermValue managePermissions = PermValue.Inherit, + PermValue manageChannel = PermValue.Inherit, PermValue readMessages = PermValue.Inherit, PermValue sendMessages = PermValue.Inherit, PermValue sendTTSMessages = PermValue.Inherit, + PermValue manageMessages = PermValue.Inherit, PermValue embedLinks = PermValue.Inherit, PermValue attachFiles = PermValue.Inherit, PermValue readMessageHistory = PermValue.Inherit, + PermValue mentionEveryone = PermValue.Inherit, PermValue connect = PermValue.Inherit, PermValue speak = PermValue.Inherit, PermValue muteMembers = PermValue.Inherit, PermValue deafenMembers = PermValue.Inherit, + PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit) + : this(0, 0, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) { } + + /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. + public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? managePermissions = null, + PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, + PermValue? moveMembers = null, PermValue? useVoiceActivation = null) + => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation); + + /// + public override string ToString() => $"Allow: {Convert.ToString(AllowValue, 2)}, Deny: {Convert.ToString(DenyValue, 2)}"; + } +} diff --git a/src/Discord.Net/Entities/Presences/GuildPresence.cs b/src/Discord.Net/Entities/Presences/GuildPresence.cs new file mode 100644 index 000000000..142e042e7 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/GuildPresence.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.MemberPresence; + +namespace Discord +{ + public class GuildPresence : Presence + { + public Guild Guild { get; } + public ulong UserId { get; } + + /// + public IReadOnlyList Roles { get; private set; } + + internal GuildPresence(ulong userId, Guild guild) + { + UserId = userId; + Guild = guild; + } + internal override void Update(Model model) + { + base.Update(model); + Roles = model.Roles.Select(x => Guild.GetRole(x)).ToImmutableArray(); + } + + public bool HasRole(Role role) => false; + + //TODO: Unsure about these + /*public Task AddRoles(params Role[] roles) => xxx; + public Task RemoveRoles(params Role[] roles) => xxx;*/ + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Presences/Presence.cs b/src/Discord.Net/Entities/Presences/Presence.cs new file mode 100644 index 000000000..671f60966 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/Presence.cs @@ -0,0 +1,18 @@ +using Model = Discord.API.MemberPresence; + +namespace Discord +{ + public class Presence + { + public string CurrentGame { get; private set; } + public UserStatus Status { get; private set; } + + internal Presence() { } + + internal virtual void Update(Model model) + { + CurrentGame = model.Game?.Name; + Status = model.Status; + } + } +} diff --git a/src/Discord.Net/Entities/Presences/VoiceState.cs b/src/Discord.Net/Entities/Presences/VoiceState.cs new file mode 100644 index 000000000..f6b56d7b6 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/VoiceState.cs @@ -0,0 +1,66 @@ +using Discord.API.Rest; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.MemberVoiceState; + +namespace Discord +{ + public class VoiceState + { + [Flags] + private enum VoiceStates : byte + { + None = 0x0, + Muted = 0x01, + Deafened = 0x02, + Suppressed = 0x4, + SelfMuted = 0x10, + SelfDeafened = 0x20, + } + + private VoiceStates _voiceStates; + + public Guild Guild { get; } + public ulong UserId { get; } + + /// Gets this user's current voice channel. + public VoiceChannel VoiceChannel { get; internal set; } + + /// Returns true if this user has marked themselves as muted. + public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; + /// Returns true if this user has marked themselves as deafened. + public bool IsSelfDeafened => (_voiceStates & VoiceStates.SelfDeafened) != 0; + /// Returns true if the guild is blocking audio from this user. + public bool IsMuted => (_voiceStates & VoiceStates.Muted) != 0; + /// Returns true if the guild is blocking audio to this user. + public bool IsDeafened => (_voiceStates & VoiceStates.Deafened) != 0; + /// Returns true if the guild is temporarily blocking audio to/from this user. + public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; + + internal VoiceState(ulong userId, Guild guild) + { + UserId = userId; + Guild = guild; + } + + internal void Update(Model model) + { + if (model.IsMuted == true) + _voiceStates |= VoiceStates.Muted; + else if (model.IsMuted == false) + _voiceStates &= ~VoiceStates.Muted; + + if (model.IsDeafened == true) + _voiceStates |= VoiceStates.Deafened; + else if (model.IsDeafened == false) + _voiceStates &= ~VoiceStates.Deafened; + + if (model.IsSuppressed == true) + _voiceStates |= VoiceStates.Suppressed; + else if (model.IsSuppressed == false) + _voiceStates &= ~VoiceStates.Suppressed; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/PrivateChannel.cs b/src/Discord.Net/Entities/PrivateChannel.cs deleted file mode 100644 index b3f3f1faf..000000000 --- a/src/Discord.Net/Entities/PrivateChannel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class PrivateChannel : Channel, IPrivateChannel, ITextChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - private readonly MessageManager _messages; - - /// Gets the target user, if this is a private chat. - public User Recipient { get; } - - public override DiscordClient Client => Recipient.Client; - - public override ChannelType Type => ChannelType.Private; - public override User CurrentUser => Client.PrivateUser; - public override IEnumerable Users => new User[] { Client.PrivateUser, Recipient }; - - internal override MessageManager MessageManager => _messages; - internal override PermissionManager PermissionManager => null; - - internal PrivateChannel(ulong id, User recipient, APIChannel model) - : this(id, recipient) - { - _messages = new MessageManager(this, Client.Config.MessageCacheSize); - Update(model); - } - private PrivateChannel(ulong id, User recipient) - :base(id) - { - Recipient = recipient; - } - - internal override User GetUser(ulong id) - { - if (id == Recipient.Id) return Recipient; - else if (id == Client.CurrentUser.Id) return Client.PrivateUser; - else return null; - } - - public Message GetMessage(ulong id) => _messages.Get(id); - public Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - => _messages.Download(limit, relativeMessageId, relativeDir); - - public Task SendMessage(string text, bool isTTS = false) => _messages.Send(text, isTTS); - public Task SendFile(string filePath) => _messages.SendFile(filePath); - public Task SendFile(string filename, Stream stream) => _messages.SendFile(filename, stream); - public Task SendIsTyping() => Client.ClientAPI.Send(new SendIsTypingRequest(Id)); - - public override string ToString() => $"@{Recipient.Name}"; - - internal override void Update(APIChannel model) { } - internal override Channel Clone() - { - var result = new PrivateChannel(Id, Recipient); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/Profile.cs b/src/Discord.Net/Entities/Profile.cs deleted file mode 100644 index 7029e4c41..000000000 --- a/src/Discord.Net/Entities/Profile.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.Rest; -using System; -using System.IO; -using System.Threading.Tasks; -using APIUser = Discord.API.Client.User; - -namespace Discord -{ - public class Profile - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this user. - public ulong Id { get; } - /// Gets the global name of this user. - public string Name => Client.PrivateUser.Name; - /// Gets the unique identifier for this user's current avatar. - public string AvatarId => Client.PrivateUser.AvatarId; - /// Gets the URL to this user's current avatar. - public string AvatarUrl => Client.PrivateUser.AvatarUrl; - /// Gets an id uniquely identifying from others with the same name. - public ushort Discriminator => Client.PrivateUser.Discriminator; - /// Gets the name of the game this user is currently playing. - public string CurrentGame => Client.PrivateUser.CurrentGame; - /// Gets the current status for this user. - public UserStatus Status => Client.PrivateUser.Status; - /// Returns the string used to mention this user. - public string Mention => $"<@{Id}>"; - - /// Gets the email for this user. - public string Email { get; private set; } - /// Gets if the email for this user has been verified. - public bool? IsVerified { get; private set; } - - internal Profile(UserReference model, DiscordClient client) - : this(model.Id, client) - { - } - private Profile(ulong id, DiscordClient client) - { - Id = id; - Client = client; - } - - internal void Update(APIUser model) - { - Email = model.Email; - IsVerified = model.IsVerified; - } - - public async Task Edit(string currentPassword = "", - string username = null, string email = null, string password = null, - Stream avatar = null, ImageType avatarType = ImageType.Png) - { - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - - var request = new UpdateProfileRequest() - { - CurrentPassword = currentPassword, - Email = email ?? Email, - Password = password, - Username = username ?? Client.PrivateUser.Name, - AvatarBase64 = avatar.Base64(avatarType, Client.PrivateUser.AvatarId) - }; - - await Client.ClientAPI.Send(request).ConfigureAwait(false); - - if (password != null) - { - var loginRequest = new LoginRequest() - { - Email = Email, - Password = password - }; - var loginResponse = await Client.ClientAPI.Send(loginRequest).ConfigureAwait(false); - Client.ClientAPI.Token = loginResponse.Token; - } - } - - internal Profile Clone() - { - var result = new Profile(Id, Client); - _cloner(this, result); - return result; - } - - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net/Entities/PublicChannel.cs b/src/Discord.Net/Entities/PublicChannel.cs deleted file mode 100644 index a1d5c4f41..000000000 --- a/src/Discord.Net/Entities/PublicChannel.cs +++ /dev/null @@ -1,109 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - /// A public Discord channel - public abstract class PublicChannel : Channel, IModel, IMentionable - { - internal readonly PermissionManager _permissions; - - /// Gets the server owning this channel. - public Server Server { get; } - - /// Gets or sets the name of this channel. - public string Name { get; set; } - /// Getsor sets the position of this channel relative to other channels of the same type in this server. - public int Position { get; set; } - - /// Gets the DiscordClient that created this model. - public override DiscordClient Client => Server.Client; - public override User CurrentUser => Server.CurrentUser; - /// Gets the string used to mention this channel. - public string Mention => $"<#{Id}>"; - /// Gets a collection of all custom permissions used for this channel. - public IEnumerable PermissionRules => _permissions.Rules; - - internal PublicChannel(APIChannel model, Server server) - : this(model.Id, server) - { - _permissions = new PermissionManager(this, model, server.Client.Config.UsePermissionsCache ? (int)(server.UserCount * 1.05) : -1); - Update(model); - } - protected PublicChannel(ulong id, Server server) - : base(id) - { - Server = server; - } - - internal override void Update(APIChannel model) - { - if (model.Name != null) Name = model.Name; - if (model.Position != null) Position = model.Position.Value; - - if (model.PermissionOverwrites != null) - _permissions.Update(model); - } - - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - public abstract Task Save(); - - internal override User GetUser(ulong id) => Server.GetUser(id); - - public ChannelTriStatePermissions? GetPermissionsRule(User user) => _permissions.GetOverwrite(user); - public ChannelTriStatePermissions? GetPermissionsRule(Role role) => _permissions.GetOverwrite(role); - public Task AddOrUpdatePermissionsRule(User user, ChannelTriStatePermissions permissions) => _permissions.AddOrUpdateOverwrite(user, permissions); - public Task AddOrUpdatePermissionsRule(Role role, ChannelTriStatePermissions permissions) => _permissions.AddOrUpdateOverwrite(role, permissions); - public Task RemovePermissionsRule(User user) => _permissions.RemoveOverwrite(user); - public async Task RemovePermissionsRule(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - try { await Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(Id, role.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - internal ChannelPermissions GetPermissions(User user) => _permissions.GetPermissions(user); - internal void UpdatePermissions() => _permissions.UpdatePermissions(); - internal void UpdatePermissions(User user) => _permissions.UpdatePermissions(user); - internal bool ResolvePermissions(User user, ref ChannelPermissions permissions) => _permissions.ResolvePermissions(user, ref permissions); - - internal override PermissionManager PermissionManager => null; - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) - { - if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); - if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); - - var request = new CreateInviteRequest(Id) - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - IsTemporary = tempMembership, - WithXkcdPass = withXkcd - }; - - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - var invite = new Invite(response, Client); - return invite; - } - - internal void AddUser(User user) => _permissions.AddUser(user); - internal void RemoveUser(ulong id) => _permissions.RemoveUser(id); - - public override string ToString() => $"{Server}/{Name ?? Id.ToIdString()}"; - } -} diff --git a/src/Discord.Net/Entities/Region.cs b/src/Discord.Net/Entities/Region.cs deleted file mode 100644 index cf144a223..000000000 --- a/src/Discord.Net/Entities/Region.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Discord -{ - public class Region - { - public string Id { get; } - public string Name { get; } - public string Hostname { get; } - public int Port { get; } - public bool Vip { get; } - - internal Region(string id, string name, string hostname, int port, bool vip) - { - Id = id; - Name = name; - Hostname = hostname; - Port = port; - Vip = vip; - } - - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net/Entities/Role.cs b/src/Discord.Net/Entities/Role.cs index 2f946f0b8..1e138c911 100644 --- a/src/Discord.Net/Entities/Role.cs +++ b/src/Discord.Net/Entities/Role.cs @@ -1,134 +1,85 @@ -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Net; using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading.Tasks; -using APIRole = Discord.API.Client.Role; +using Model = Discord.API.Role; namespace Discord { - public class Role : IMentionable + public class Role : IEntity, IMentionable { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public DiscordClient Client => Server.Client; - - /// Gets the unique identifier for this role. + /// public ulong Id { get; } - /// Gets the server this role is a member of. - public Server Server { get; } + /// Returns the guild this role belongs to. + public Guild Guild { get; } /// Gets the name of this role. public string Name { get; private set; } - /// If true, this role is displayed isolated from other users. - public bool IsHoisted { get; private set; } - /// Gets the position of this channel relative to other channels in this server. + /// Returns true if members of this role are isolated in the user list. + public bool IsHoisted { get; private set; } + /// Gets the position of this role relative to other roles in this guild. public int Position { get; private set; } - /// Gets whether this role is managed by server (e.g. for Twitch integration) + /// Returns true if this role is managed by the Discord server (e.g. for Twitch integration) public bool IsManaged { get; private set; } - /// Gets the the permissions given to this role. - public ServerPermissions Permissions { get; private set; } - /// Gets the color of this role. + /// Gets the permissions given to all members of this role. + public GuildPermissions Permissions { get; private set; } + /// Gets the color assigned to members of this role. public Color Color { get; private set; } - - /// Gets true if this is the role representing all users in a server. - public bool IsEveryone => Id == Server.Id; - /// Gets a list of all members in this role. - public IEnumerable Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this)); + /// + public DiscordClient Discord => Guild.Discord; + /// Returns true if this is the role representing all users in a server. + public bool IsEveryone => Id == Guild.Id; /// Gets the string used to mention this role. public string Mention => IsEveryone ? "@everyone" : ""; + /// Gets a collection of all members in this role. + public IEnumerable Members { get { throw new NotImplementedException(); } } //TODO: Implement - internal Role(ulong id, Server server) - { + internal Role(ulong id, Guild guild) + { Id = id; - Server = server; - - Permissions = new ServerPermissions(0); - Color = new Color(0); - } + Guild = guild; + } - internal void Update(APIRole model, bool updatePermissions) - { - if (model.Name != null) - Name = model.Name; - if (model.Hoist != null) - IsHoisted = model.Hoist.Value; - if (model.Managed != null) - IsManaged = model.Managed.Value; - if (model.Position != null && !IsEveryone) - Position = model.Position.Value; - if (model.Color != null) - Color = new Color(model.Color.Value); - if (model.Permissions != null) - { - Permissions = new ServerPermissions(model.Permissions.Value); - if (updatePermissions) //Dont update these during READY - { - foreach (var member in Members) - Server.UpdatePermissions(member); - } - } - } - - public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null) + internal void Update(Model model) { - var updateRequest = new UpdateRoleRequest(Server.Id, Id) - { - Name = name ?? Name, - Permissions = (permissions ?? Permissions).RawValue, - Color = (color ?? Color).RawValue, - IsHoisted = isHoisted ?? IsHoisted - }; - - var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false); + Name = model.Name; + IsHoisted = model.Hoist.Value; + IsManaged = model.Managed.Value; + Position = model.Position.Value; + Color = new Color(model.Color.Value); + Permissions = new GuildPermissions(model.Permissions.Value); + } - if (position != null) - { - int oldPos = Position; - int newPos = position.Value; - int minPos; - Role[] roles = Server.Roles.OrderBy(x => x.Position).ToArray(); + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - roles[i] = roles[i + 1]; - roles[newPos] = this; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - roles[i] = roles[i - 1]; - roles[newPos] = this; - } + /// Modifies the properties of this role. + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); - var reorderRequest = new ReorderRolesRequest(Server.Id) - { - RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), - StartPos = minPos - }; - await Client.ClientAPI.Send(reorderRequest).ConfigureAwait(false); - } + var req = new ModifyGuildRoleRequest(Guild.Id, Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); } + /// Deletes this message. public async Task Delete() { - try { await Client.ClientAPI.Send(new DeleteRoleRequest(Server.Id, Id)).ConfigureAwait(false); } + try { await Discord.RestClient.Send(new DeleteGuildRoleRequest(Guild.Id, Id)).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - internal Role Clone() + internal void UpdatePermissions() { - var result = new Role(Id, Server); - _cloner(this, result); - return result; + foreach (var member in Members) + Guild.UpdatePermissions(member); } - public override string ToString() => $"{Server}/{Name ?? Id.ToString()}"; + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; } } diff --git a/src/Discord.Net/Entities/Server.cs b/src/Discord.Net/Entities/Server.cs deleted file mode 100644 index a5e586126..000000000 --- a/src/Discord.Net/Entities/Server.cs +++ /dev/null @@ -1,511 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using APIMember = Discord.API.Client.Member; -using Discord.API.Client; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - /// Represents a Discord server (also known as a guild). - public class Server - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - internal static string GetIconUrl(ulong serverId, string iconId) - => iconId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{serverId}/icons/{iconId}.jpg" : null; - internal static string GetSplashUrl(ulong serverId, string splashId) - => splashId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{serverId}/splashes/{splashId}.jpg" : null; - - public class Emoji - { - public string Id { get; } - - public string Name { get; internal set; } - public bool IsManaged { get; internal set; } - public bool RequireColons { get; internal set; } - public IEnumerable Roles { get; internal set; } - - internal Emoji(string id) - { - Id = id; - } - } - private struct Member - { - public readonly User User; - public readonly ServerPermissions Permissions; - public Member(User user, ServerPermissions permissions) - { - User = user; - Permissions = permissions; - } - } - - private ConcurrentDictionary _roles; - private ConcurrentDictionary _users; - private ConcurrentDictionary _channels; - private ulong _ownerId; - private ulong? _afkChannelId; - private int _userCount; - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this server. - public ulong Id { get; } - - /// Gets the name of this server. - public string Name { get; set; } - /// Gets the voice region for this server. - public Region Region { get; set; } - /// Gets the AFK voice channel for this server. - public VoiceChannel AFKChannel { get; set; } - /// Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK voice channel, if one is set. - public int AFKTimeout { get; set; } - - /// Gets the date and time you joined this server. - public DateTime JoinedAt { get; private set; } - /// Gets the the role representing all users in a server. - public Role EveryoneRole { get; private set; } - /// Gets all extra features added to this server. - public IEnumerable Features { get; private set; } - /// Gets all custom emojis on this server. - public IEnumerable CustomEmojis { get; private set; } - /// Gets the unique identifier for this user's current avatar. - public string IconId { get; private set; } - /// Gets the unique identifier for this server's custom splash image. - public string SplashId { get; private set; } - - /// Gets the user that created this server. - public User Owner => GetUser(_ownerId); - /// Gets the default channel for this server. - public TextChannel DefaultChannel => _channels[Id] as TextChannel; - /// Gets the current user in this server. - public User CurrentUser => GetUser(Client.CurrentUser.Id); - /// Gets the URL to this server's current icon. - public string IconUrl => GetIconUrl(Id, IconId); - /// Gets the URL to this servers's splash image. - public string SplashUrl => GetSplashUrl(Id, SplashId); - - /// Gets a collection of all channels in this server. - public IEnumerable AllChannels => _channels.Select(x => x.Value); - /// Gets a collection of text channels in this server. - public IEnumerable TextChannels => _channels.Where(x => x.Value.IsText).Select(x => x.Value as TextChannel); - /// Gets a collection of voice channels in this server. - public IEnumerable VoiceChannels => _channels.Where(x => x.Value.IsVoice).Select(x => x.Value as VoiceChannel); - /// Gets a collection of all members in this server. - public IEnumerable Users => _users.Select(x => x.Value.User); - /// Gets a collection of all roles in this server. - public IEnumerable Roles => _roles.Select(x => x.Value); - - /// Gets the number of channels in this server. - public int ChannelCount => _channels.Count; - /// Gets the number of users downloaded for this server so far. - internal int CurrentUserCount => _users.Count; - /// Gets the number of users in this server. - public int UserCount => _userCount; - /// Gets the number of roles in this server. - public int RoleCount => _roles.Count; - - internal Server(ulong id, DiscordClient client) - { - Id = id; - Client = client; - } - - internal void Update(Guild model) - { - if (model.Name != null) - Name = model.Name; - if (model.AFKTimeout != null) - AFKTimeout = model.AFKTimeout.Value; - if (model.JoinedAt != null) - JoinedAt = model.JoinedAt.Value; - if (model.OwnerId != null) - _ownerId = model.OwnerId.Value; - if (model.Region != null) - Region = Client.GetRegion(model.Region); - if (model.Icon != null) - IconId = model.Icon; - if (model.Features != null) - Features = model.Features; - if (model.Roles != null) - { - _roles = new ConcurrentDictionary(2, model.Roles.Length); - foreach (var x in model.Roles) - { - var role = AddRole(x.Id); - role.Update(x, false); - } - EveryoneRole = _roles[Id]; - } - if (model.Emojis != null) //Needs Roles - { - CustomEmojis = model.Emojis.Select(x => new Emoji(x.Id) - { - Name = x.Name, - IsManaged = x.IsManaged, - RequireColons = x.RequireColons, - Roles = x.RoleIds.Select(y => GetRole(y)).Where(y => y != null).ToArray() - }).ToArray(); - } - - //Can be null - _afkChannelId = model.AFKChannelId; - SplashId = model.Splash; - } - internal void Update(ExtendedGuild model) - { - Update(model as Guild); - - //Only channels or members should have AddXXX(cachePerms: true), not both - if (model.Channels != null) - { - _channels = new ConcurrentDictionary(2, (int)(model.Channels.Length * 1.05)); - foreach (var subModel in model.Channels) - AddChannel(subModel, false); - } - if (model.MemberCount != null) - { - if (_users == null) - _users = new ConcurrentDictionary(2, (int)(model.MemberCount * 1.05)); - _userCount = model.MemberCount.Value; - } - if (!model.IsLarge) - { - if (model.Members != null) - { - foreach (var subModel in model.Members) - AddUser(subModel, true, false).Update(subModel); - } - if (model.VoiceStates != null) - { - foreach (var subModel in model.VoiceStates) - GetUser(subModel.UserId)?.Update(subModel); - } - if (model.Presences != null) - { - foreach (var subModel in model.Presences) - GetUser(subModel.User.Id)?.Update(subModel); - } - } - } - - /// Edits this server, changing only non-null attributes. - public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) - { - var request = new UpdateGuildRequest(Id) - { - Name = name ?? Name, - Region = region ?? Region.Id, - IconBase64 = icon.Base64(iconType, IconId), - AFKChannelId = AFKChannel?.Id, - AFKTimeout = AFKTimeout, - Splash = SplashId - }; - return Client.ClientAPI.Send(request); - } - - /// Leaves this server. This function will fail if you're the owner - use Delete instead. - public async Task Leave() - { - try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - /// Deletes this server. This function will fail if you're not the owner - use Leave instead. - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteGuildRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - #region Bans - public async Task> GetBans() - { - var response = await Client.ClientAPI.Send(new GetBansRequest(Id)).ConfigureAwait(false); - return response.Select(x => - { - var user = new User(x, Client, this); - return user; - }); - } - - public Task Ban(User user, int pruneDays = 0) - { - var request = new AddGuildBanRequest(user.Server.Id, user.Id) - { - PruneDays = pruneDays - }; - return Client.ClientAPI.Send(request); - } - public Task Unban(User user, int pruneDays = 0) - => Unban(user.Id); - public async Task Unban(ulong userId) - { - try { await Client.ClientAPI.Send(new RemoveGuildBanRequest(Id, userId)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - #endregion - - #region Channels - internal PublicChannel AddChannel(APIChannel model, bool cachePerms) - { - PublicChannel channel; - ChannelType type = EnumConverters.ToChannelType(model.Type); - if (type == ChannelType.Voice) - channel = new VoiceChannel(model, this); - else - channel = new TextChannel(model, this); - - if (cachePerms && Client.Config.UsePermissionsCache) - { - foreach (var user in Users) - channel.AddUser(user); - } - Client.AddChannel(channel); - return _channels.GetOrAdd(model.Id, x => channel); - } - internal PublicChannel RemoveChannel(ulong id) - { - PublicChannel channel; - _channels.TryRemove(id, out channel); - return channel; - } - - /// Gets the channel with the provided id and owned by this server, or null if not found. - public PublicChannel GetChannel(ulong id) - { - PublicChannel result; - _channels.TryGetValue(id, out result); - return result; - } - public TextChannel GetTextChannel(ulong id) => GetChannel(id) as TextChannel; - public VoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as VoiceChannel; - - /// Creates a new channel. - public async Task CreateChannel(string name, ChannelType type) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (type != ChannelType.Text && type != ChannelType.Voice) throw new ArgumentException("Invalid channel type", nameof(type)); - - var request = new CreateChannelRequest(Id) { Name = name, Type = type }; - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - - var channel = AddChannel(response, true); - channel.Update(response); - return channel; - } - #endregion - - #region Invites - /// Gets all active (non-expired) invites to this server. - public async Task> DownloadInvites() - { - var response = await Client.ClientAPI.Send(new GetInvitesRequest(Id)).ConfigureAwait(false); - return response.Select(x => - { - var invite = new Invite(x, Client); - invite.Update(x); - return invite; - }); - } - - /// Creates a new invite to the default channel of this server. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) - => DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); - #endregion - - #region Roles - internal Role AddRole(ulong id) - => _roles.GetOrAdd(id, x => new Role(x, this)); - internal Role RemoveRole(ulong id) - { - Role role; - _roles.TryRemove(id, out role); - return role; - } - - /// Gets the role with the provided id and owned by this server, or null if not found. - public Role GetRole(ulong id) - { - Role result; - _roles.TryGetValue(id, out result); - return result; - } - - /// Creates a new role. - public async Task CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var createRequest = new CreateRoleRequest(Id); - var createResponse = await Client.ClientAPI.Send(createRequest).ConfigureAwait(false); - var role = AddRole(createResponse.Id); - role.Update(createResponse, false); - - var editRequest = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? Color.Default).RawValue, - IsHoisted = isHoisted - }; - var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false); - role.Update(editResponse, true); - - return role; - } - - /// Reorders the provided roles and optionally places them after a certain role. - public Task ReorderRoles(IEnumerable roles, Role after = null) - { - if (roles == null) throw new ArgumentNullException(nameof(roles)); - - return Client.ClientAPI.Send(new ReorderRolesRequest(Id) - { - RoleIds = roles.Select(x => x.Id).ToArray(), - StartPos = after != null ? after.Position + 1 : roles.Min(x => x.Position) - }); - } - #endregion - - #region Permissions - internal ServerPermissions GetPermissions(User user) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - return member.Permissions; - else - return ServerPermissions.None; - } - - internal void UpdatePermissions(User user) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - { - var perms = member.Permissions; - if (UpdatePermissions(member.User, ref perms)) - { - _users[user.Id] = new Member(member.User, perms); - foreach (var channel in _channels) - channel.Value.UpdatePermissions(user); - } - } - } - - private bool UpdatePermissions(User user, ref ServerPermissions permissions) - { - uint newPermissions = 0; - - if (user.Id == _ownerId) - newPermissions = ServerPermissions.All.RawValue; - else - { - foreach (var serverRole in user.Roles) - newPermissions |= serverRole.Permissions.RawValue; - } - - if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) - newPermissions = ServerPermissions.All.RawValue; - - if (newPermissions != permissions.RawValue) - { - permissions = new ServerPermissions(newPermissions); - return true; - } - return false; - } - #endregion - - #region Users - internal User AddUser(APIMember model, bool cachePerms, bool incrementCount) - { - if (incrementCount) - _userCount++; - - Member member; - if (!_users.TryGetValue(model.User.Id, out member)) //Users can only be added from websocket thread, ignore threadsafety - { - member = new Member(new User(model, Client, this), ServerPermissions.None); - if (model.User.Id == Client.CurrentUser.Id) - { - member.User.CurrentGame = Client.CurrentGame; - member.User.Status = Client.Status; - } - - _users[model.User.Id] = member; - if (cachePerms && Client.Config.UsePermissionsCache) - { - foreach (var channel in _channels) - channel.Value.AddUser(member.User); - } - } - return member.User; - } - internal User RemoveUser(ulong id) - { - _userCount--; - Member member; - if (_users.TryRemove(id, out member)) - { - foreach (var channel in _channels) - channel.Value.RemoveUser(id); - return member.User; - } - return null; - } - - /// Gets the user with the provided id and is a member of this server, or null if not found. - public User GetUser(ulong id) - { - Member result; - if (_users.TryGetValue(id, out result)) - return result.User; - else - return null; - } - /// Gets the user with the provided username and discriminator, that is a member of this server, or null if not found. - public User GetUser(string name, ushort discriminator) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - return _users.Select(x => x.Value.User).Where(x => x.Discriminator == discriminator && x.Name == name).SingleOrDefault(); - } - - /// Kicks all users with an inactivity greater or equal to the provided number of days. - /// If true, no pruning will actually be done but instead return the number of users that would be pruned. - public async Task PruneUsers(int days = 30, bool simulate = false) - { - if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); - - var request = new PruneMembersRequest(Id) - { - Days = days, - IsSimulation = simulate - }; - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - return response.Pruned; - } - #endregion - - internal Server Clone() - { - var result = new Server(Id, Client); - _cloner(this, result); - return result; - } - - public override string ToString() => Name ?? Id.ToIdString(); - } -} diff --git a/src/Discord.Net/Entities/TextChannel.cs b/src/Discord.Net/Entities/TextChannel.cs deleted file mode 100644 index 8ee97aad1..000000000 --- a/src/Discord.Net/Entities/TextChannel.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using APIChannel = Discord.API.Client.Channel; - -namespace Discord -{ - public class TextChannel : PublicChannel, IPublicChannel, ITextChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - private readonly MessageManager _messages; - - /// Gets or sets the topic of this channel. - public string Topic { get; set; } - - public override ChannelType Type => ChannelType.Text; - /// Gets a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. - public IEnumerable Messages => _messages != null ? _messages : Enumerable.Empty(); - /// Gets a collection of all users with read access to this channel. - public override IEnumerable Users - { - get - { - if (Client.Config.UsePermissionsCache) - return _permissions.Users.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); - else - { - ChannelPermissions perms = new ChannelPermissions(); - return Server.Users.Where(x => - { - _permissions.ResolvePermissions(x, ref perms); - return perms.ReadMessages == true; - }); - } - } - } - - internal override MessageManager MessageManager => _messages; - - internal TextChannel(APIChannel model, Server server) - : base(model, server) - { - if (Client.Config.MessageCacheSize > 0) - _messages = new MessageManager(this, (int)(Client.Config.MessageCacheSize * 1.05)); - } - private TextChannel(ulong id, Server server) - : base(id, server) - { - } - - internal override void Update(APIChannel model) - { - base.Update(model); - if (model.Topic != null) Topic = model.Topic; - } - /// Save all changes to this channel. - public override async Task Save() - { - var request = new UpdateChannelRequest(Id) - { - Name = Name, - Topic = Topic, - Position = Position - }; - await Client.ClientAPI.Send(request).ConfigureAwait(false); - } - - public Message GetMessage(ulong id) => _messages.Get(id); - public Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - => _messages.Download(limit, relativeMessageId, relativeDir); - - public Task SendMessage(string text, bool isTTS = false) => _messages.Send(text, isTTS); - public Task SendFile(string filePath) => _messages.SendFile(filePath); - public Task SendFile(string filename, Stream stream) => _messages.SendFile(filename, stream); - public Task SendIsTyping() => Client.ClientAPI.Send(new SendIsTypingRequest(Id)); - - internal override Channel Clone() - { - var result = new TextChannel(Id, Server); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/User.cs b/src/Discord.Net/Entities/User.cs deleted file mode 100644 index 89a0b1133..000000000 --- a/src/Discord.Net/Entities/User.cs +++ /dev/null @@ -1,356 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using APIMember = Discord.API.Client.Member; - -namespace Discord -{ - public class User - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - internal static string GetAvatarUrl(ulong userId, string avatarId) - => avatarId != null ? $"{DiscordConfig.ClientAPIUrl}users/{userId}/avatars/{avatarId}.jpg" : null; - - [Flags] - private enum VoiceState : byte - { - Normal = 0x0, - SelfMuted = 0x01, - SelfDeafened = 0x02, - ServerMuted = 0x10, - ServerDeafened = 0x20, - ServerSuppressed = 0x40, - } - - internal struct CompositeKey : IEquatable - { - public ulong ServerId, UserId; - public CompositeKey(ulong userId, ulong? serverId) - { - ServerId = serverId ?? 0; - UserId = userId; - } - - public bool Equals(CompositeKey other) - => UserId == other.UserId && ServerId == other.ServerId; - public override int GetHashCode() - => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); - } - - private VoiceState _voiceState; - private DateTime? _lastOnline; - private ulong? _voiceChannelId; - private Dictionary _roles; - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this user. - public ulong Id { get; } - /// Gets the server this user is a member of. - public Server Server { get; } - - /// Gets the name of this user. - public string Name { get; private set; } - /// Gets an id uniquely identifying from others with the same name. - public ushort Discriminator { get; private set; } - /// Gets the unique identifier for this user's current avatar. - public string AvatarId { get; private set; } - /// Gets the name of the game this user is currently playing. - public string CurrentGame { get; internal set; } - /// Gets the current status for this user. - public UserStatus Status { get; internal set; } - /// Gets the datetime that this user joined this server. - public DateTime JoinedAt { get; private set; } - /// Returns the time this user last sent/edited a message, started typing or sent voice data in this server. - public DateTime? LastActivityAt { get; private set; } - // /// Gets this user's voice session id. - // public string SessionId { get; private set; } - // /// Gets this user's voice token. - // public string Token { get; private set; } - - /// Gets the current private channel for this user if one exists. - public PrivateChannel PrivateChannel => Client.GetPrivateChannel(Id); - /// Returns the string used to mention this user. - public string Mention => $"<@{Id}>"; - public bool IsOwner => Server == null ? false : this == Server.Owner; - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0; - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened => (_voiceState & VoiceState.SelfDeafened) != 0; - /// Returns true if the server is blocking audio from this user. - public bool IsServerMuted => (_voiceState & VoiceState.ServerMuted) != 0; - /// Returns true if the server is blocking audio to this user. - public bool IsServerDeafened => (_voiceState & VoiceState.ServerDeafened) != 0; - /// Returns true if the server is temporarily blocking audio to/from this user. - public bool IsServerSuppressed => (_voiceState & VoiceState.ServerSuppressed) != 0; - /// Returns the time this user was last seen online in this server. - public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel => _voiceChannelId != null ? Server.GetVoiceChannel(_voiceChannelId.Value) : null; - /// Gets the URL to this user's current avatar. - public string AvatarUrl => GetAvatarUrl(Id, AvatarId); - /// Gets all roles that have been assigned to this user, including the everyone role. - public IEnumerable Roles => _roles.Select(x => x.Value); - public ServerPermissions ServerPermissions => Server.GetPermissions(this); - - /// Returns a collection of all channels this user has permissions to join on this server. - public IEnumerable Channels - { - get - { - if (Server != null) - { - if (Client.Config.UsePermissionsCache) - { - return Server.AllChannels.Where(x => - (x.IsText && x.GetPermissions(this).ReadMessages) || - (x.IsVoice && x.GetPermissions(this).Connect)); - } - else - { - ChannelPermissions perms = new ChannelPermissions(); - return Server.AllChannels - .Where(x => - { - x.ResolvePermissions(this, ref perms); - return (x.Type == ChannelType.Text && perms.ReadMessages) || - (x.Type == ChannelType.Voice && perms.Connect); - }); - } - } - else - { - if (this == Client.PrivateUser) - return Client.PrivateChannels; - else - { - var privateChannel = Client.GetPrivateChannel(Id); - if (privateChannel != null) - return new IChannel[] { privateChannel }; - else - return new IChannel[0]; - } - } - } - } - - internal User(ExtendedMember model, DiscordClient client, Server server) - : this(model as APIMember, client, server) - { - if (model.IsServerMuted == true) - _voiceState |= VoiceState.ServerMuted; - else if (model.IsServerMuted == false) - _voiceState &= ~VoiceState.ServerMuted; - - if (model.IsServerDeafened == true) - _voiceState |= VoiceState.ServerDeafened; - else if (model.IsServerDeafened == false) - _voiceState &= ~VoiceState.ServerDeafened; - } - internal User(APIMember model, DiscordClient client, Server server) - : this(model.User.Id, client, server) - { - if (server == null) - UpdateRoles(null); - Update(model); - } - internal User(UserReference model, DiscordClient client, Server server) - : this(model.Id, client, server) - { - if (server == null) - UpdateRoles(null); - Update(model); - } - private User(ulong id, DiscordClient client, Server server) - { - Client = client; - Id = id; - Server = server; - - _roles = new Dictionary(); - Status = UserStatus.Offline; - } - - internal void Update(UserReference model) - { - if (model.Username != null) - Name = model.Username; - if (model.Discriminator != null) - Discriminator = model.Discriminator.Value; - if (model.Avatar != null) - AvatarId = model.Avatar; - } - internal void Update(APIMember model) - { - if (model.User != null) - Update(model.User); - - if (model.JoinedAt.HasValue) - JoinedAt = model.JoinedAt.Value; - if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); - } - internal void Update(MemberPresence model) - { - if (model.User != null) - Update(model.User as UserReference); - - if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); - if (model.Status != null && Status != model.Status) - { - Status = UserStatus.FromString(model.Status); - if (Status == UserStatus.Offline) - _lastOnline = DateTime.UtcNow; - } - - CurrentGame = model.Game?.Name; //Allows null - } - internal void Update(MemberVoiceState model) - { - if (model.IsSelfMuted == true) - _voiceState |= VoiceState.SelfMuted; - else if (model.IsSelfMuted == false) - _voiceState &= ~VoiceState.SelfMuted; - if (model.IsSelfDeafened == true) - _voiceState |= VoiceState.SelfDeafened; - else if (model.IsSelfDeafened == false) - _voiceState &= ~VoiceState.SelfDeafened; - if (model.IsServerMuted == true) - _voiceState |= VoiceState.ServerMuted; - else if (model.IsServerMuted == false) - _voiceState &= ~VoiceState.ServerMuted; - if (model.IsServerDeafened == true) - _voiceState |= VoiceState.ServerDeafened; - else if (model.IsServerDeafened == false) - _voiceState &= ~VoiceState.ServerDeafened; - if (model.IsServerSuppressed == true) - _voiceState |= VoiceState.ServerSuppressed; - else if (model.IsServerSuppressed == false) - _voiceState &= ~VoiceState.ServerSuppressed; - - /*if (model.SessionId != null) - SessionId = model.SessionId; - if (model.Token != null) - Token = model.Token;*/ - - _voiceChannelId = model.ChannelId; //Allows null - } - - internal void UpdateActivity(DateTime? activity = null) - { - if (LastActivityAt == null || activity > LastActivityAt.Value) - LastActivityAt = activity ?? DateTime.UtcNow; - } - - public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) - { - if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel"); - - //Modify the roles collection and filter out the everyone role - var roleIds = (roles ?? Roles) - .Where(x => !x.IsEveryone) - .Select(x => x.Id) - .Distinct() - .ToArray(); - - var request = new UpdateMemberRequest(Server.Id, Id) - { - IsMuted = isMuted ?? IsServerMuted, - IsDeafened = isDeafened ?? IsServerDeafened, - VoiceChannelId = voiceChannel?.Id, - RoleIds = roleIds - }; - return Client.ClientAPI.Send(request); - } - - public Task Kick() - { - if (Server == null) throw new InvalidOperationException("Unable to kick users from a private channel"); - - var request = new KickMemberRequest(Server.Id, Id); - return Client.ClientAPI.Send(request); - } - - public ChannelPermissions GetPermissions(PublicChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return channel.GetPermissions(this); - } - - public Task CreatePMChannel() => Client.CreatePrivateChannel(this); - - private void UpdateRoles(IEnumerable roles) - { - bool updated = false; - var newRoles = new Dictionary(); - - var oldRoles = _roles; - if (roles != null) - { - foreach (var r in roles) - { - if (r != null) - { - newRoles[r.Id] = r; - if (!oldRoles.ContainsKey(r.Id)) - updated = true; //Check for adds - } - } - } - - if (Server != null) - { - var everyone = Server.EveryoneRole; - newRoles[everyone.Id] = everyone; - } - if (oldRoles.Count != newRoles.Count) - updated = true; //Check for removes - - if (updated) - { - _roles = newRoles; - if (Server != null) - Server.UpdatePermissions(this); - } - } - public bool HasRole(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - - return _roles.ContainsKey(role.Id); - } - - public Task AddRoles(params Role[] roles) => Edit(roles: Roles.Concat(roles)); - public Task AddRoles(IEnumerable roles) => Edit(roles: Roles.Concat(roles)); - public Task RemoveRoles(params Role[] roles) => Edit(roles: Roles.Except(roles)); - public Task RemoveRoles(IEnumerable roles) => Edit(roles: Roles.Except(roles)); - - internal User Clone() - { - var result = new User(Id, Client, Server); - _cloner(this, result); - return result; - } - - public override string ToString() - { - if (Name != null) - return $"{Server?.Name ?? "[Private]"}/{Name}#{Discriminator}"; - else - return $"{Server?.Name ?? "[Private]"}/{Id}"; - } - internal string ToString(IChannel channel) - { - if (Name != null) - return $"{channel}/{Name}#{Discriminator}"; - else - return $"{channel}/{Id}"; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/DMUser.cs b/src/Discord.Net/Entities/Users/DMUser.cs new file mode 100644 index 000000000..ed2c7d541 --- /dev/null +++ b/src/Discord.Net/Entities/Users/DMUser.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class DMUser : User + { + public DMChannel Channel { get; } + + public override DiscordClient Discord => Channel.Discord; + + internal DMUser(ulong id, DMChannel channel) + : base(id) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs new file mode 100644 index 000000000..bc0c8db8c --- /dev/null +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -0,0 +1,48 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.GuildMember; + +namespace Discord +{ + public class GuildUser : User, IMentionable + { + public Guild Guild { get; } + public GuildPresence Presence { get; } + public VoiceState VoiceState { get; } + + /// + public DateTime JoinedAt { get; private set; } + public GuildPermissions GuildPermissions { get; internal set; } + + public override DiscordClient Discord => Guild.Discord; + public IEnumerable TextChannels => Guild.TextChannels.Where(x => GetPermissions(x).ReadMessages); + + internal GuildUser(ulong id, Guild guild, GuildPresence presence, VoiceState voiceState) + : base(id) + { + Guild = guild; + Presence = presence; + VoiceState = voiceState; + } + internal void Update(Model model) + { + base.Update(model.User); + JoinedAt = model.JoinedAt.Value; + } + + public ChannelPermissions GetPermissions(GuildChannel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + return channel.GetPermissions(this); + } + + /// + public override Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + public Task Kick() => Discord.RestClient.Send(new RemoveGuildMemberRequest(Guild.Id, Id)); + public Task Ban(int pruneDays = 0) => Discord.RestClient.Send(new CreateGuildBanRequest(Guild.Id, Id) { PruneDays = pruneDays }); + public Task Unban() => Discord.RestClient.Send(new RemoveGuildBanRequest(Guild.Id, Id)); + } +} diff --git a/src/Discord.Net/Entities/Users/PublicUser.cs b/src/Discord.Net/Entities/Users/PublicUser.cs new file mode 100644 index 000000000..2ab851a25 --- /dev/null +++ b/src/Discord.Net/Entities/Users/PublicUser.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public class PublicUser : User + { + public override DiscordClient Discord { get; } + + internal PublicUser(ulong id, DiscordClient discord) + : base(id) + { + Discord = discord; + } + } +} diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs new file mode 100644 index 000000000..4174672cf --- /dev/null +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -0,0 +1,26 @@ +using Model = Discord.API.User; + +namespace Discord +{ + public class SelfUser : User + { + public override DiscordClient Discord { get; } + + public string Email { get; private set; } + public bool IsVerified { get; private set; } + + internal SelfUser(ulong id, DiscordClient discord) + : base(id) + { + Discord = discord; + } + + internal override void Update(Model model) + { + base.Update(model); + + Email = model.Email; + IsVerified = model.IsVerified; + } + } +} diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs new file mode 100644 index 000000000..92c5a2773 --- /dev/null +++ b/src/Discord.Net/Entities/Users/User.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord +{ + public abstract class User : IEntity + { + private string _avatarId; + + /// + public ulong Id { get; } + /// + public abstract DiscordClient Discord { get; } + + public string Username { get; private set; } + public ushort Discriminator { get; private set; } + public bool IsBot { get; private set; } + + public string AvatarUrl => CDN.GetUserAvatarUrl(Id, _avatarId); + public string Mention => MentionHelper.Mention(this); + + internal User(ulong id) + { + Id = id; + } + internal virtual void Update(Model model) + { + Username = model.Username; + Discriminator = model.Discriminator; + IsBot = model.Bot; + _avatarId = model.Avatar; + } + + public virtual Task Update() { throw new NotSupportedException(); } + + public async Task CreateDMChannel() => await Discord.GetOrCreateDMChannel(Id); //TODO: We dont want both this and .Channel to appear on DMUser + } +} diff --git a/src/Discord.Net/Entities/VoiceChannel.cs b/src/Discord.Net/Entities/VoiceChannel.cs deleted file mode 100644 index 5b40a6aad..000000000 --- a/src/Discord.Net/Entities/VoiceChannel.cs +++ /dev/null @@ -1,60 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Discord.API.Client.Rest; - -namespace Discord -{ - public class VoiceChannel : PublicChannel, IPublicChannel, IVoiceChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public int Bitrate { get; set; } - - public override ChannelType Type => ChannelType.Public | ChannelType.Voice; - /// Gets a collection of all users currently in this voice channel. - public override IEnumerable Users - { - get - { - if (Client.Config.UsePermissionsCache) - return _permissions.Users.Select(x => x.User).Where(x => x.VoiceChannel == this); - else - return Server.Users.Where(x => x.VoiceChannel == this); - } - } - - internal override MessageManager MessageManager => null; - - internal VoiceChannel(APIChannel model, Server server) - : base(model, server) - { - } - private VoiceChannel(ulong id, Server server) - : base(id, server) - { - } - - - /// Save all changes to this channel. - public override async Task Save() - { - var request = new UpdateChannelRequest(Id) - { - Name = Name, - Position = Position, - Bitrate = Bitrate - }; - await Client.ClientAPI.Send(request).ConfigureAwait(false); - } - - internal override Channel Clone() - { - var result = new VoiceChannel(Id, Server); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/VoiceRegion.cs b/src/Discord.Net/Entities/VoiceRegion.cs new file mode 100644 index 000000000..bba703240 --- /dev/null +++ b/src/Discord.Net/Entities/VoiceRegion.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.Rest.GetVoiceRegionsResponse; + +namespace Discord +{ + public class VoiceRegion : IEntity + { + /// + public string Id { get; } + /// + public DiscordClient Discord { get; } + + /// Gets the name of this voice region. + public string Name { get; private set; } + /// Returns true if this voice region is exclusive to VIP accounts. + public bool IsVip { get; private set; } + /// Returns true if this voice region is the closest to your machine. + public bool IsOptimal { get; private set; } + /// Gets an example hostname for this voice region. + public string SampleHostname { get; private set; } + /// Gets an example port for this voice region. + public int SamplePort { get; private set; } + + internal VoiceRegion(string id, DiscordClient client) + { + Id = id; + Discord = client; + } + + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + public void Update(Model model) + { + Name = model.Name; + IsVip = model.IsVip; + IsOptimal = model.IsOptimal; + SampleHostname = model.SampleHostname; + SamplePort = model.SamplePort; + } + + public override string ToString() + { + string suffix = ""; + + if (IsVip) + { + if (IsOptimal) + suffix = " (VIP, Optimal)"; + else + suffix = " (VIP)"; + } + else if (IsOptimal) + suffix = " (Optimal)"; + + return Name + suffix; + } + } +} diff --git a/src/Discord.Net/EnumConverters.cs b/src/Discord.Net/EnumConverters.cs deleted file mode 100644 index 7448055f6..000000000 --- a/src/Discord.Net/EnumConverters.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace Discord -{ - public static class EnumConverters - { - public static ChannelType ToChannelType(string value) - { - switch (value) - { - case "text": return ChannelType.Text; - case "voice": return ChannelType.Voice; - default: throw new ArgumentException("Unknown channel type", nameof(value)); - } - } - public static string ToString(ChannelType value) - { - if ((value & ChannelType.Text) != 0) return "text"; - if ((value & ChannelType.Voice) != 0) return "voice"; - throw new ArgumentException("Invalid channel tType", nameof(value)); - } - - public static PermissionTarget ToPermissionTarget(string value) - { - switch (value) - { - case "member": return PermissionTarget.User; - case "role": return PermissionTarget.Role; - default: throw new ArgumentException("Unknown permission target", nameof(value)); - } - } - public static string ToString(PermissionTarget value) - { - switch (value) - { - case PermissionTarget.User: return "member"; - case PermissionTarget.Role: return "role"; - default: throw new ArgumentException("Invalid permission target", nameof(value)); - } - } - } -} diff --git a/src/Discord.Net/Enums/ChannelType.cs b/src/Discord.Net/Enums/ChannelType.cs index 77b0614ea..e6a3a1e00 100644 --- a/src/Discord.Net/Enums/ChannelType.cs +++ b/src/Discord.Net/Enums/ChannelType.cs @@ -1,13 +1,9 @@ -using System; - -namespace Discord +namespace Discord { - [Flags] - public enum ChannelType : byte + public enum ChannelType : byte { - Public = 0x01, - Private = 0x02, - Text = 0x10, - Voice = 0x20 + DM, + Text, + Voice } } diff --git a/src/Discord.Net/Enums/ConnectionState.cs b/src/Discord.Net/Enums/ConnectionState.cs index 42c505ccd..dfd4ac9eb 100644 --- a/src/Discord.Net/Enums/ConnectionState.cs +++ b/src/Discord.Net/Enums/ConnectionState.cs @@ -1,6 +1,6 @@ namespace Discord { - public enum ConnectionState : byte + public enum ConnectionState { Disconnected, Connecting, diff --git a/src/Discord.Net/Enums/ImageType.cs b/src/Discord.Net/Enums/ImageType.cs deleted file mode 100644 index 738c67a3d..000000000 --- a/src/Discord.Net/Enums/ImageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum ImageType - { - None, - Jpeg, - Png - } -} diff --git a/src/Discord.Net/Enums/LogSeverity.cs b/src/Discord.Net/Enums/LogSeverity.cs index c62d8c250..785b0ef46 100644 --- a/src/Discord.Net/Enums/LogSeverity.cs +++ b/src/Discord.Net/Enums/LogSeverity.cs @@ -1,7 +1,8 @@ namespace Discord { - public enum LogSeverity : byte + public enum LogSeverity { + Critical = 0, Error = 1, Warning = 2, Info = 3, diff --git a/src/Discord.Net/Enums/MessageState.cs b/src/Discord.Net/Enums/MessageState.cs deleted file mode 100644 index 59f65614d..000000000 --- a/src/Discord.Net/Enums/MessageState.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Discord -{ - public enum MessageState : byte - { - /// Message did not originate from this session, or was successfully sent. - Normal = 0, - /// Message is current queued. - Queued, - /// Message was deleted. - Deleted, - /// Message was deleted before it was sent. - Aborted, - /// Message failed to be sent. - Failed, - /// Message has been removed from cache and will no longer receive updates. - Detached - } -} diff --git a/src/Discord.Net/Enums/PermissionBits.cs b/src/Discord.Net/Enums/PermissionBits.cs index 0766dadc4..ac51dfb05 100644 --- a/src/Discord.Net/Enums/PermissionBits.cs +++ b/src/Discord.Net/Enums/PermissionBits.cs @@ -1,6 +1,6 @@ namespace Discord { - internal enum PermissionBits : byte + internal enum PermissionBit : byte { //General CreateInstantInvite = 0, @@ -8,7 +8,7 @@ BanMembers = 2, ManageRolesOrPermissions = 3, ManageChannel = 4, - ManageServer = 5, + ManageGuild = 5, //Text ReadMessages = 10, diff --git a/src/Discord.Net/Enums/PermissionTarget.cs b/src/Discord.Net/Enums/PermissionTarget.cs index d1381a5ec..96595fb69 100644 --- a/src/Discord.Net/Enums/PermissionTarget.cs +++ b/src/Discord.Net/Enums/PermissionTarget.cs @@ -1,8 +1,8 @@ namespace Discord { - public enum PermissionTarget : byte - { - User, - Role + public enum PermissionTarget + { + Role, + User } } diff --git a/src/Discord.Net/Enums/Relative.cs b/src/Discord.Net/Enums/Relative.cs index 4bd44c5ab..aade047d1 100644 --- a/src/Discord.Net/Enums/Relative.cs +++ b/src/Discord.Net/Enums/Relative.cs @@ -2,6 +2,7 @@ { public enum Relative { - Before, After + Before, + After } } diff --git a/src/Discord.Net/Enums/StringEnum.cs b/src/Discord.Net/Enums/StringEnum.cs deleted file mode 100644 index 903bdfdba..000000000 --- a/src/Discord.Net/Enums/StringEnum.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public abstract class StringEnum - { - public string Value { get; } - - protected StringEnum(string value) - { - Value = value; - } - - public override string ToString() => Value; - } -} diff --git a/src/Discord.Net/Enums/UserStatus.cs b/src/Discord.Net/Enums/UserStatus.cs index 80def4234..f2fdfda7c 100644 --- a/src/Discord.Net/Enums/UserStatus.cs +++ b/src/Discord.Net/Enums/UserStatus.cs @@ -1,40 +1,9 @@ namespace Discord { - public class UserStatus : StringEnum - { - /// User is currently online and active. - public static UserStatus Online { get; } = new UserStatus("online"); - /// User is currently online but inactive. - public static UserStatus Idle { get; } = new UserStatus("idle"); - /// User is offline. - public static UserStatus Offline { get; } = new UserStatus("offline"); - - private UserStatus(string value) - : base(value) { } - - public static UserStatus FromString(string value) - { - switch (value) - { - case null: - return null; - case "online": - return Online; - case "idle": - return Idle; - case "offline": - return Offline; - default: - return new UserStatus(value); - } - } - - - public static implicit operator UserStatus(string value) => FromString(value); - public static bool operator ==(UserStatus a, UserStatus b) => ((object)a == null && (object)b == null) || (a?.Equals(b) ?? false); - public static bool operator !=(UserStatus a, UserStatus b) => !(a == b); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals(object obj) => (obj as UserStatus)?.Equals(this) ?? false; - public bool Equals(UserStatus type) => type != null && type.Value == Value; + public enum UserStatus + { + Online, + Idle, + Offline } } diff --git a/src/Discord.Net/Events/ChannelEventArgs.cs b/src/Discord.Net/Events/ChannelEventArgs.cs index ac31a27f5..b03db540f 100644 --- a/src/Discord.Net/Events/ChannelEventArgs.cs +++ b/src/Discord.Net/Events/ChannelEventArgs.cs @@ -6,6 +6,9 @@ namespace Discord { public IChannel Channel { get; } - public ChannelEventArgs(IChannel channel) { Channel = channel; } + public ChannelEventArgs(IChannel channel) + { + Channel = channel; + } } } diff --git a/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs index 138f2dd8e..b4fbc258f 100644 --- a/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs @@ -1,16 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class ChannelUpdatedEventArgs : EventArgs + public class ChannelUpdatedEventArgs : ChannelEventArgs { public IChannel Before { get; } - public IChannel After { get; } + public IChannel After => Channel; public ChannelUpdatedEventArgs(IChannel before, IChannel after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/CurrentUserEventArgs.cs b/src/Discord.Net/Events/CurrentUserEventArgs.cs new file mode 100644 index 000000000..a947bbfc5 --- /dev/null +++ b/src/Discord.Net/Events/CurrentUserEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class CurrentUserEventArgs : EventArgs + { + public SelfUser CurrentUser { get; } + + public CurrentUserEventArgs(SelfUser currentUser) + { + CurrentUser = currentUser; + } + } +} diff --git a/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs b/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs new file mode 100644 index 000000000..4309f0312 --- /dev/null +++ b/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public class CurrentUserUpdatedEventArgs : CurrentUserEventArgs + { + public SelfUser Before { get; } + public SelfUser After => CurrentUser; + + public CurrentUserUpdatedEventArgs(SelfUser before, SelfUser after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/Events/DisconnectedEventArgs.cs b/src/Discord.Net/Events/DisconnectedEventArgs.cs index 87f9ec955..cf7c1bf70 100644 --- a/src/Discord.Net/Events/DisconnectedEventArgs.cs +++ b/src/Discord.Net/Events/DisconnectedEventArgs.cs @@ -7,10 +7,10 @@ namespace Discord public bool WasUnexpected { get; } public Exception Exception { get; } - public DisconnectedEventArgs(bool wasUnexpected, Exception ex) + public DisconnectedEventArgs(bool wasUnexpected, Exception exception = null) { WasUnexpected = wasUnexpected; - Exception = ex; + Exception = exception; } } } diff --git a/src/Discord.Net/Events/GuildEventArgs.cs b/src/Discord.Net/Events/GuildEventArgs.cs new file mode 100644 index 000000000..50625d882 --- /dev/null +++ b/src/Discord.Net/Events/GuildEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class GuildEventArgs : EventArgs + { + public Guild Guild { get; } + + public GuildEventArgs(Guild guild) + { + Guild = guild; + } + } +} diff --git a/src/Discord.Net/Events/GuildUpdatedEventArgs.cs b/src/Discord.Net/Events/GuildUpdatedEventArgs.cs new file mode 100644 index 000000000..9258072f7 --- /dev/null +++ b/src/Discord.Net/Events/GuildUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public class GuildUpdatedEventArgs : GuildEventArgs + { + public Guild Before { get; } + public Guild After => Guild; + + public GuildUpdatedEventArgs(Guild before, Guild after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/Events/LogMessageEventArgs.cs b/src/Discord.Net/Events/LogMessageEventArgs.cs index bd1fa5b93..83de48616 100644 --- a/src/Discord.Net/Events/LogMessageEventArgs.cs +++ b/src/Discord.Net/Events/LogMessageEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace Discord { @@ -9,12 +10,54 @@ namespace Discord public string Message { get; } public Exception Exception { get; } - public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) + public LogMessageEventArgs(LogSeverity severity, string source, string message, Exception exception = null) { Severity = severity; Source = source; - Message = msg; + Message = message; Exception = exception; } + + public override string ToString() => ToString(null, true); + + public string ToString(StringBuilder builder = null, bool fullException = true) + { + string sourceName = Source; + string message = Message; + string exMessage = fullException ? Exception?.ToString() : Exception?.Message; + + int maxLength = 1 + (sourceName?.Length ?? 0) + 2 + (message?.Length ?? 0) + 3 + (exMessage?.Length ?? 0); + if (builder == null) + builder = new StringBuilder(maxLength); + else + { + builder.Clear(); + builder.EnsureCapacity(maxLength); + } + + if (sourceName != null) + { + builder.Append('['); + builder.Append(sourceName); + 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 (exMessage != null) + { + builder.AppendLine(":"); + builder.Append(exMessage); + } + + return builder.ToString(); + } } } diff --git a/src/Discord.Net/Events/MessageEventArgs.cs b/src/Discord.Net/Events/MessageEventArgs.cs index 76c9455dc..366b16dbb 100644 --- a/src/Discord.Net/Events/MessageEventArgs.cs +++ b/src/Discord.Net/Events/MessageEventArgs.cs @@ -6,10 +6,9 @@ namespace Discord { public Message Message { get; } - public User User => Message.User; - public ITextChannel Channel => Message.Channel; - public Server Server => Message.Server; - - public MessageEventArgs(Message msg) { Message = msg; } + public MessageEventArgs(Message message) + { + Message = message; + } } } diff --git a/src/Discord.Net/Events/MessageUpdatedEventArgs.cs b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs index 1583dc981..318ac9d54 100644 --- a/src/Discord.Net/Events/MessageUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs @@ -1,20 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class MessageUpdatedEventArgs : EventArgs + public class MessageUpdatedEventArgs : MessageEventArgs { public Message Before { get; } - public Message After { get; } - - public User User => After.User; - public ITextChannel Channel => After.Channel; - public Server Server => After.Server; + public Message After => Message; public MessageUpdatedEventArgs(Message before, Message after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs b/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs deleted file mode 100644 index 2365908e8..000000000 --- a/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Discord -{ - public class ProfileUpdatedEventArgs : EventArgs - { - public Profile Before { get; } - public Profile After { get; } - - public ProfileUpdatedEventArgs(Profile before, Profile after) - { - Before = before; - After = after; - } - } -} diff --git a/src/Discord.Net/Events/RoleEventArgs.cs b/src/Discord.Net/Events/RoleEventArgs.cs index 13eb0f7f4..887972ffe 100644 --- a/src/Discord.Net/Events/RoleEventArgs.cs +++ b/src/Discord.Net/Events/RoleEventArgs.cs @@ -6,8 +6,9 @@ namespace Discord { public Role Role { get; } - public Server Server => Role.Server; - - public RoleEventArgs(Role role) { Role = role; } + public RoleEventArgs(Role role) + { + Role = role; + } } } diff --git a/src/Discord.Net/Events/RoleUpdatedEventArgs.cs b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs index 26151c98b..285ab42c5 100644 --- a/src/Discord.Net/Events/RoleUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs @@ -1,18 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class RoleUpdatedEventArgs : EventArgs + public class RoleUpdatedEventArgs : RoleEventArgs { public Role Before { get; } - public Role After { get; } - - public Server Server => After.Server; + public Role After => Role; public RoleUpdatedEventArgs(Role before, Role after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/ServerEventArgs.cs b/src/Discord.Net/Events/ServerEventArgs.cs deleted file mode 100644 index e9e564e1b..000000000 --- a/src/Discord.Net/Events/ServerEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerEventArgs : EventArgs - { - public Server Server { get; } - - public ServerEventArgs(Server server) { Server = server; } - } -} diff --git a/src/Discord.Net/Events/ServerUpdatedEventArgs.cs b/src/Discord.Net/Events/ServerUpdatedEventArgs.cs deleted file mode 100644 index 8532f72dc..000000000 --- a/src/Discord.Net/Events/ServerUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerUpdatedEventArgs : EventArgs - { - public Server Before { get; } - public Server After { get; } - - public ServerUpdatedEventArgs(Server before, Server after) - { - Before = before; - After = after; - } - } -} diff --git a/src/Discord.Net/Events/TypingEventArgs.cs b/src/Discord.Net/Events/TypingEventArgs.cs index 73d47f688..797d0d9d6 100644 --- a/src/Discord.Net/Events/TypingEventArgs.cs +++ b/src/Discord.Net/Events/TypingEventArgs.cs @@ -1,11 +1,13 @@ -namespace Discord +using System; + +namespace Discord { - public class TypingEventArgs + public class TypingEventArgs : EventArgs { - public ITextChannel Channel { get; } + public IMessageChannel Channel { get; } public User User { get; } - public TypingEventArgs(ITextChannel channel, User user) + public TypingEventArgs(IMessageChannel channel, User user) { Channel = channel; User = user; diff --git a/src/Discord.Net/Events/UserEventArgs.cs b/src/Discord.Net/Events/UserEventArgs.cs index bf7dd2cac..6bbcaef01 100644 --- a/src/Discord.Net/Events/UserEventArgs.cs +++ b/src/Discord.Net/Events/UserEventArgs.cs @@ -1,12 +1,14 @@ using System; + namespace Discord { public class UserEventArgs : EventArgs { public User User { get; } - public Server Server => User.Server; - - public UserEventArgs(User user) { User = user; } + public UserEventArgs(User user) + { + User = user; + } } } diff --git a/src/Discord.Net/Events/UserUpdatedEventArgs.cs b/src/Discord.Net/Events/UserUpdatedEventArgs.cs index 89e8cce0c..35b68bc55 100644 --- a/src/Discord.Net/Events/UserUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/UserUpdatedEventArgs.cs @@ -1,17 +1,14 @@ -using System; -namespace Discord +namespace Discord { - public class UserUpdatedEventArgs : EventArgs + public class UserUpdatedEventArgs : UserEventArgs { public User Before { get; } - public User After { get; } - - public Server Server => After.Server; + public User After => User; public UserUpdatedEventArgs(User before, User after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/VoiceChannelEventArgs.cs b/src/Discord.Net/Events/VoiceChannelEventArgs.cs new file mode 100644 index 000000000..78d519d2f --- /dev/null +++ b/src/Discord.Net/Events/VoiceChannelEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class VoiceChannelEventArgs : EventArgs + { + public VoiceChannel Channel { get; } + + public VoiceChannelEventArgs(VoiceChannel channel) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net/Format.cs index 0ff7e0a4f..1e8ae00b9 100644 --- a/src/Discord.Net/Format.cs +++ b/src/Discord.Net/Format.cs @@ -2,109 +2,104 @@ namespace Discord { - public static class Format - { - private static readonly string[] _patterns; - private static readonly StringBuilder _builder; + public static class Format + { + private static readonly string[] _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`" }; + private static readonly StringBuilder _builder = new StringBuilder(DiscordConfig.MaxMessageSize); - static Format() - { - _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; - _builder = new StringBuilder(DiscordConfig.MaxMessageSize); - } - - /// Removes all special formatting characters from the provided text. - public static string Escape(string text) - { - lock (_builder) - { - _builder.Clear(); + /// Removes all special formatting characters from the provided text. + public static string Escape(string text) + { + //TODO: Fix me + lock (_builder) + { + _builder.Clear(); - //Escape all backslashes - for (int i = 0; i < text.Length; i++) - { - _builder.Append(text[i]); - if (text[i] == '\\') - _builder.Append('\\'); - } + //Escape all backslashes + for (int i = 0; i < text.Length; i++) + { + _builder.Append(text[i]); + if (text[i] == '\\') + _builder.Append('\\'); + } - EscapeSubstring(0, _builder.Length); + EscapeSubstring(0, _builder.Length); - return _builder.ToString(); - } - } - private static int EscapeSubstring(int start, int end) - { - int totalAddedChars = 0; - for (int i = start; i < end + totalAddedChars; i++) - { - for (int p = 0; p < _patterns.Length; p++) - { - string pattern = _patterns[p]; - if (i + pattern.Length * 2 > _builder.Length) - continue; - int s = FindPattern(pattern, i, i + 1); - if (s == -1) continue; - int e = FindPattern(pattern, i + 1, end + totalAddedChars); - if (e == -1) continue; + return _builder.ToString(); + } + } + private static int EscapeSubstring(int start, int end) + { + int totalAddedChars = 0; + for (int i = start; i < end + totalAddedChars; i++) + { + for (int p = 0; p < _patterns.Length; p++) + { + string pattern = _patterns[p]; + if (i + pattern.Length * 2 > _builder.Length) + continue; + int s = FindPattern(pattern, i, i + 1); + if (s == -1) continue; + int e = FindPattern(pattern, i + 1, end + totalAddedChars); + if (e == -1) continue; - if (e - s - pattern.Length > 0) - { - //By going right to left, we dont need to adjust any offsets - for (int k = pattern.Length - 1; k >= 0; k--) - _builder.Insert(e + k, '\\'); - for (int k = pattern.Length - 1; k >= 0; k--) - _builder.Insert(s + k, '\\'); + if (e - s - pattern.Length > 0) + { + //By going right to left, we dont need to adjust any offsets + for (int k = pattern.Length - 1; k >= 0; k--) + _builder.Insert(e + k, '\\'); + for (int k = pattern.Length - 1; k >= 0; k--) + _builder.Insert(s + k, '\\'); int addedChars = pattern.Length * 2; - addedChars += EscapeSubstring(s + pattern.Length * 2, e + pattern.Length); - i = e + addedChars + pattern.Length - 1; - totalAddedChars += addedChars; - break; + addedChars += EscapeSubstring(s + pattern.Length * 2, e + pattern.Length); + i = e + addedChars + pattern.Length - 1; + totalAddedChars += addedChars; + break; } - } - } - return totalAddedChars; - } - private static int FindPattern(string pattern, int start, int end) - { - for (int j = start; j < end; j++) - { - if (_builder[j] == '\\') - { - j++; - continue; - } - for (int k = 0; k < pattern.Length; k++) - { - if (_builder[j + k] != pattern[k]) - goto nextpos; - } - return j; - nextpos:; - } - return -1; - } - - /// Returns a markdown-formatted string with bold formatting, optionally escaping the contents. - public static string Bold(string text, bool escape = true) - => escape ? $"**{Escape(text)}**" : $"**{text}**"; - /// Returns a markdown-formatted string with italics formatting, optionally escaping the contents. - public static string Italics(string text, bool escape = true) - => escape ? $"*{Escape(text)}*" : $"*{text}*"; - /// Returns a markdown-formatted string with underline formatting, optionally escaping the contents. - public static string Underline(string text, bool escape = true) - => escape ? $"__{Escape(text)}__" : $"__{text}__"; - /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. - public static string Strikeout(string text, bool escape = true) - => escape ? $"~~{Escape(text)}~~" : $"~~{text}~~"; + } + } + return totalAddedChars; + } + private static int FindPattern(string pattern, int start, int end) + { + for (int j = start; j < end; j++) + { + if (_builder[j] == '\\') + { + j++; + continue; + } + for (int k = 0; k < pattern.Length; k++) + { + if (_builder[j + k] != pattern[k]) + goto nextpos; + } + return j; + nextpos:; + } + return -1; + } + + /// Returns a markdown-formatted string with bold formatting, optionally escaping the contents. + public static string Bold(string text, bool escape = true) + => escape ? $"**{Escape(text)}**" : $"**{text}**"; + /// Returns a markdown-formatted string with italics formatting, optionally escaping the contents. + public static string Italics(string text, bool escape = true) + => escape ? $"*{Escape(text)}*" : $"*{text}*"; + /// Returns a markdown-formatted string with underline formatting, optionally escaping the contents. + public static string Underline(string text, bool escape = true) + => escape ? $"__{Escape(text)}__" : $"__{text}__"; + /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. + public static string Strikeout(string text, bool escape = true) + => escape ? $"~~{Escape(text)}~~" : $"~~{text}~~"; - /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. - public static string Code(string text, string language = null) - { - if (language != null || text.Contains("\n")) - return $"```{language ?? ""}\n{text}\n```"; - else - return $"`{text}`"; - } - } + /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. + public static string Code(string text, string language = null) + { + if (language != null || text.Contains("\n")) + return $"```{language ?? ""}\n{text}\n```"; + else + return $"`{text}`"; + } + } } diff --git a/src/Discord.Net/IMentionable.cs b/src/Discord.Net/IMentionable.cs deleted file mode 100644 index 0a4bf439c..000000000 --- a/src/Discord.Net/IMentionable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IMentionable - { - string Mention { get; } - } -} diff --git a/src/Discord.Net/IModel.cs b/src/Discord.Net/IModel.cs deleted file mode 100644 index 8a86ceb3d..000000000 --- a/src/Discord.Net/IModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public interface IModel - { - ulong Id { get; } - - Task Save(); - } -} diff --git a/src/Discord.Net/IService.cs b/src/Discord.Net/IService.cs deleted file mode 100644 index 15f79b0c4..000000000 --- a/src/Discord.Net/IService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IService - { - void Install(DiscordClient client); - } -} diff --git a/src/Discord.Net/InternalExtensions.cs b/src/Discord.Net/InternalExtensions.cs index 676f0f8e6..6b8342e57 100644 --- a/src/Discord.Net/InternalExtensions.cs +++ b/src/Discord.Net/InternalExtensions.cs @@ -1,75 +1,22 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Runtime.CompilerServices; - -namespace Discord +namespace Discord { internal static class InternalExtensions { - internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; - - public static ulong ToId(this string value) => ulong.Parse(value, NumberStyles.None, _format); - public static ulong? ToNullableId(this string value) => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); - public static bool TryToId(this string value, out ulong result) => ulong.TryParse(value, NumberStyles.None, _format, out result); - - public static string ToIdString(this ulong value) => value.ToString(_format); - public static string ToIdString(this ulong? value) => value?.ToString(_format); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasBit(this uint rawValue, byte bit) => ((rawValue >> bit) & 1U) == 1; - - public static bool TryGetOrAdd(this ConcurrentDictionary d, - TKey key, Func factory, out TValue result) - { - bool created = false; - TValue newValue = default(TValue); - while (true) - { - if (d.TryGetValue(key, out result)) - return false; - if (!created) - { - newValue = factory(key); - created = true; - } - if (d.TryAdd(key, newValue)) - { - result = newValue; - return true; - } - } - } - public static bool TryGetOrAdd(this ConcurrentDictionary d, - TKey key, TValue value, out TValue result) + public static User GetCurrentUser(this IChannel channel) { - while (true) + switch (channel.Type) { - if (d.TryGetValue(key, out result)) - return false; - if (d.TryAdd(key, value)) - { - result = value; - return true; - } + case ChannelType.Text: + case ChannelType.Voice: + return (channel as GuildChannel).Guild.CurrentUser; + default: + return channel.Discord.CurrentUser; } } - public static string Base64(this Stream stream, ImageType type, string existingId) - { - if (type == ImageType.None) - return null; - else if (stream != null) - { - byte[] bytes = new byte[stream.Length - stream.Position]; - stream.Read(bytes, 0, bytes.Length); - - string base64 = Convert.ToBase64String(bytes); - string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; - return $"data:{imageType},{base64}"; - } - return existingId; - } +/*#if NETSTANDARD1_2 + //https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs + public static long ToUnixTimeMilliseconds(this DateTimeOffset dto) => (dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond) - 62135596800000; +#endif*/ } } diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs deleted file mode 100644 index abc713cc2..000000000 --- a/src/Discord.Net/Logging/ILogger.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace Discord.Logging -{ - public interface ILogger - { - LogSeverity Level { get; } - - void Log(LogSeverity severity, string message, Exception exception = null); -#if DOTNET5_4 - void Log(LogSeverity severity, FormattableString message, Exception exception = null); -#endif - - void Error(string message, Exception exception = null); -#if DOTNET5_4 - void Error(FormattableString message, Exception exception = null); -#endif - void Error(Exception exception); - - void Warning(string message, Exception exception = null); -#if DOTNET5_4 - void Warning(FormattableString message, Exception exception = null); -#endif - void Warning(Exception exception); - - void Info(string message, Exception exception = null); -#if DOTNET5_4 - void Info(FormattableString message, Exception exception = null); -#endif - void Info(Exception exception); - - void Verbose(string message, Exception exception = null); -#if DOTNET5_4 - void Verbose(FormattableString message, Exception exception = null); -#endif - void Verbose(Exception exception); - - void Debug(string message, Exception exception = null); -#if DOTNET5_4 - void Debug(FormattableString message, Exception exception = null); -#endif - void Debug(Exception exception); - } -} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 2abc4f10d..d6cc5f435 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -4,72 +4,61 @@ namespace Discord.Logging { public class LogManager { - private readonly DiscordClient _client; - public LogSeverity Level { get; } public event EventHandler Message = delegate { }; - internal LogManager(DiscordClient client) - { - _client = client; - Level = client.Config.LogLevel; + internal LogManager(LogSeverity minSeverity) + { + Level = minSeverity; } public void Log(LogSeverity severity, string source, string message, Exception exception = null) { if (severity <= Level) - { - try { Message(this, new LogMessageEventArgs(severity, source, message, exception)); } - catch { } //We dont want to log on log errors - } + Message(this, new LogMessageEventArgs(severity, source, message, exception)); } - -#if DOTNET5_4 public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null) { if (severity <= Level) - { - try { Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } - catch { } //We dont want to log on log errors - } + Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } -#endif public void Error(string source, string message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); + public void Error(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Error, source, message, ex); public void Error(string source, Exception ex) => Log(LogSeverity.Error, source, (string)null, ex); + public void Warning(string source, string message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); + public void Warning(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Warning, source, message, ex); public void Warning(string source, Exception ex) => Log(LogSeverity.Warning, source, (string)null, ex); + public void Info(string source, string message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); + public void Info(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Info, source, message, ex); public void Info(string source, Exception ex) => Log(LogSeverity.Info, source, (string)null, ex); + public void Verbose(string source, string message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); + public void Verbose(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Verbose, source, message, ex); public void Verbose(string source, Exception ex) => Log(LogSeverity.Verbose, source, (string)null, ex); + public void Debug(string source, string message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, Exception ex) - => Log(LogSeverity.Debug, source, (string)null, ex); - -#if DOTNET5_4 - public void Error(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Error, source, message, ex); - public void Warning(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Warning, source, message, ex); - public void Info(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Info, source, message, ex); - public void Verbose(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Verbose, source, message, ex); public void Debug(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); -#endif + public void Debug(string source, Exception ex) + => Log(LogSeverity.Debug, source, (string)null, ex); - public Logger CreateLogger(string name) => new Logger(this, name); + internal Logger CreateLogger(string name) => new Logger(this, name); } } diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 59d591163..f64520591 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -2,7 +2,7 @@ namespace Discord.Logging { - public class Logger : ILogger + public class Logger { private readonly LogManager _manager; @@ -17,40 +17,42 @@ namespace Discord.Logging public void Log(LogSeverity severity, string message, Exception exception = null) => _manager.Log(severity, Name, message, exception); + public void Log(LogSeverity severity, FormattableString message, Exception exception = null) + => _manager.Log(severity, Name, message, exception); + public void Error(string message, Exception exception = null) => _manager.Error(Name, message, exception); + public void Error(FormattableString message, Exception exception = null) + => _manager.Error(Name, message, exception); public void Error(Exception exception) => _manager.Error(Name, exception); + public void Warning(string message, Exception exception = null) => _manager.Warning(Name, message, exception); + public void Warning(FormattableString message, Exception exception = null) + => _manager.Warning(Name, message, exception); public void Warning(Exception exception) => _manager.Warning(Name, exception); + public void Info(string message, Exception exception = null) => _manager.Info(Name, message, exception); + public void Info(FormattableString message, Exception exception = null) + => _manager.Info(Name, message, exception); public void Info(Exception exception) => _manager.Info(Name, exception); + public void Verbose(string message, Exception exception = null) => _manager.Verbose(Name, message, exception); + public void Verbose(FormattableString message, Exception exception = null) + => _manager.Verbose(Name, message, exception); public void Verbose(Exception exception) => _manager.Verbose(Name, exception); + public void Debug(string message, Exception exception = null) => _manager.Debug(Name, message, exception); - public void Debug(Exception exception) - => _manager.Debug(Name, exception); - -#if DOTNET5_4 - public void Log(LogSeverity severity, FormattableString message, Exception exception = null) - => _manager.Log(severity, Name, message, exception); - public void Error(FormattableString message, Exception exception = null) - => _manager.Error(Name, message, exception); - public void Warning(FormattableString message, Exception exception = null) - => _manager.Warning(Name, message, exception); - public void Info(FormattableString message, Exception exception = null) - => _manager.Info(Name, message, exception); - public void Verbose(FormattableString message, Exception exception = null) - => _manager.Verbose(Name, message, exception); public void Debug(FormattableString message, Exception exception = null) => _manager.Debug(Name, message, exception); -#endif + public void Debug(Exception exception) + => _manager.Debug(Name, exception); } } diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs index c1a0948b0..cdbbcc251 100644 --- a/src/Discord.Net/MessageQueue.cs +++ b/src/Discord.Net/MessageQueue.cs @@ -1,5 +1,6 @@ -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Logging; +using Discord.Net; using Discord.Net.Rest; using System; using System.Collections.Concurrent; @@ -7,203 +8,242 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net + +namespace Discord { /// Manages an outgoing message queue for DiscordClient. - public class MessageQueue + public class MessageQueue : IDisposable { + private struct MessageSend + { + public readonly TaskCompletionSource Promise; + public readonly IMessageChannel Channel; + public readonly bool IsTTS; + public readonly string Text; + + public MessageSend(TaskCompletionSource promise, IMessageChannel channel, bool isTTS, string text) + { + Promise = promise; + Channel = channel; + IsTTS = isTTS; + Text = text; + } + } private struct MessageEdit { + public readonly TaskCompletionSource Promise; public readonly Message Message; public readonly string NewText; - public MessageEdit(Message message, string newText) + public MessageEdit(TaskCompletionSource promise, Message message, string newText) { + Promise = promise; Message = message; NewText = newText; } } + private struct MessageDelete + { + public readonly TaskCompletionSource Promise; + public readonly Message Message; - private const int WarningStart = 30; + public MessageDelete(TaskCompletionSource promise, Message message) + { + Promise = promise; + Message = message; + } + } - private readonly Random _nonceRand; + private const int WarningStart = 30; + private readonly RestClient _rest; private readonly Logger _logger; - private readonly ConcurrentQueue _pendingSends; + private readonly ConcurrentQueue _pendingSends; private readonly ConcurrentQueue _pendingEdits; - private readonly ConcurrentQueue _pendingDeletes; - private readonly ConcurrentDictionary _pendingSendsByNonce; + private readonly ConcurrentQueue _pendingDeletes; + private readonly SemaphoreSlim _connectionLock; private int _count, _nextWarning; + private Task[] _tasks; + private bool _isDisposed = false; /// Gets the current number of queued actions. public int Count => _count; - /// Gets the current number of queued sends. - public int SendCount => _pendingSends.Count; - /// Gets the current number of queued edits. - public int EditCount => _pendingEdits.Count; - /// Gets the current number of queued deletes. - public int DeleteCount => _pendingDeletes.Count; internal MessageQueue(RestClient rest, Logger logger) { _rest = rest; _logger = logger; _nextWarning = WarningStart; - - _nonceRand = new Random(); - _pendingSends = new ConcurrentQueue(); + + _connectionLock = new SemaphoreSlim(1, 1); + _pendingSends = new ConcurrentQueue(); _pendingEdits = new ConcurrentQueue(); - _pendingDeletes = new ConcurrentQueue(); - _pendingSendsByNonce = new ConcurrentDictionary(); + _pendingDeletes = new ConcurrentQueue(); } - - internal Message QueueSend(ITextChannel channel, string text, bool isTTS) - { - Message msg = new Message(0, channel, (channel as Channel).CurrentUser); - msg.IsTTS = isTTS; - msg.RawText = text; - msg.Text = Message.ResolveMentions(msg.Channel, msg.Text); - msg.Nonce = GenerateNonce(); - if (_pendingSendsByNonce.TryAdd(msg.Nonce, text)) - { - msg.State = MessageState.Queued; - IncrementCount(); - _pendingSends.Enqueue(msg); - } - else - msg.State = MessageState.Failed; - return msg; - } - internal void QueueEdit(Message msg, string text) + protected virtual void Dispose(bool disposing) { - string msgText = msg.RawText; - if (msg.State == MessageState.Queued && _pendingSendsByNonce.TryUpdate(msg.Nonce, text, msgText)) + if (!_isDisposed) { - //Successfully edited the message before it was sent. - return; + if (disposing) + _connectionLock.Dispose(); + _isDisposed = true; } - IncrementCount(); - _pendingEdits.Enqueue(new MessageEdit(msg, text)); } - internal void QueueDelete(Message msg) + public void Dispose() => Dispose(true); + + internal async Task Start(CancellationToken cancelToken) { - string ignored; - if (msg.State == MessageState.Queued && _pendingSendsByNonce.TryRemove(msg.Nonce, out ignored)) + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - //Successfully stopped the message from being sent - msg.State = MessageState.Aborted; - return; + await StartInternal(cancelToken).ConfigureAwait(false); } - IncrementCount(); - _pendingDeletes.Enqueue(msg); + finally { _connectionLock.Release(); } } - - internal Task[] Run(CancellationToken cancelToken) + internal async Task StartInternal(CancellationToken cancelToken) { - return new[] + await StopInternal().ConfigureAwait(false); + + _tasks = new Task[] { RunSendQueue(cancelToken), RunEditQueue(cancelToken), RunDeleteQueue(cancelToken) }; } + internal async Task Stop() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await StopInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task StopInternal() + { + if (_tasks != null) + { + await Task.WhenAll(_tasks).ConfigureAwait(false); + _tasks = null; + } + } + + internal Task QueueSend(IMessageChannel channel, string text, bool isTTS) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingSends.Enqueue(new MessageSend(promise, channel, isTTS, text)); + return promise.Task; + } + internal Task QueueEdit(Message msg, string text) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingEdits.Enqueue(new MessageEdit(promise, msg, text)); + return promise.Task; + } + internal Task QueueDelete(Message msg) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingDeletes.Enqueue(new MessageDelete(promise, msg)); + return promise.Task; + } + private Task RunSendQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - Message msg; - while (_pendingSends.TryDequeue(out msg)) + while (!cancelToken.IsCancellationRequested) { - DecrementCount(); - string text; - if (_pendingSendsByNonce.TryRemove(msg.Nonce, out text)) //If it was deleted from queue, this will fail + MessageSend item; + while (_pendingSends.TryDequeue(out item)) { try { - //msg.RawText = text; - //msg.Text = Message.ResolveMentions(msg.Channel, text); - var request = new SendMessageRequest(msg.Channel.Id) + var request = new CreateMessageRequest(item.Channel.Id) { - Content = text, - Nonce = msg.Nonce.ToString(), - IsTTS = msg.IsTTS + Content = item.Text, + IsTTS = item.IsTTS }; var response = await _rest.Send(request).ConfigureAwait(false); - msg.Id = response.Id; - msg.State = MessageState.Normal; - msg.Update(response); - } - catch (Exception ex) - { - msg.State = MessageState.Failed; - _logger.Error($"Failed to send message to {msg.Channel}", ex); + item.Promise.SetResult(item.Channel.Discord.CreateMessage(item.Channel, item.Channel.GetCurrentUser(), response)); } + catch (Exception ex) { item.Promise.SetException(ex); } + DecrementCount(); } + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } - await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } + catch (OperationCanceledException) { } }); } private Task RunEditQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - MessageEdit edit; - while (_pendingEdits.TryPeek(out edit) && edit.Message.State != MessageState.Queued) + while (!cancelToken.IsCancellationRequested) { - if (_pendingEdits.TryDequeue(out edit)) + MessageEdit item; + while (_pendingEdits.TryDequeue(out item)) { - DecrementCount(); - if (edit.Message.State == MessageState.Normal) - { + var msg = item.Message; + //if (msg.State != EntityState.Deleted) + //{ try { - var request = new UpdateMessageRequest(edit.Message.Channel.Id, edit.Message.Id) + var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id) { - Content = edit.NewText + Content = item.NewText }; await _rest.Send(request).ConfigureAwait(false); + item.Promise.SetResult(null); } - catch (Exception ex) { _logger.Error($"Failed to edit message {edit.Message}", ex); } - } + catch (Exception ex) { item.Promise.SetException(ex); } + //} + DecrementCount(); } + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } - await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } + catch (OperationCanceledException) { } }); } private Task RunDeleteQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - Message msg; - while (_pendingDeletes.TryPeek(out msg) && msg.State != MessageState.Queued) + while (!cancelToken.IsCancellationRequested) { - if (_pendingDeletes.TryDequeue(out msg)) + MessageDelete item; + while (_pendingDeletes.TryDequeue(out item)) { - DecrementCount(); - if (msg.State == MessageState.Normal) - { + var msg = item.Message; + //if (msg.State != EntityState.Deleted) + //{ try { var request = new DeleteMessageRequest(msg.Channel.Id, msg.Id); await _rest.Send(request).ConfigureAwait(false); - msg.State = MessageState.Deleted; + item.Promise.SetResult(null); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } //Ignore - catch (Exception ex) { _logger.Error($"Failed to delete message {msg}", ex); } - } + catch (Exception ex) { item.Promise.SetException(ex); } + //} + DecrementCount(); } - } - await Task.Delay(Discord.DiscordConfig.MessageQueueInterval).ConfigureAwait(false); + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); + } } + catch (OperationCanceledException) { } }); } @@ -232,22 +272,16 @@ namespace Discord.Net /// Clears all queued message sends/edits/deletes. public void Clear() { - Message msg; + MessageSend send; MessageEdit edit; + MessageDelete delete; - while (_pendingSends.TryDequeue(out msg)) + while (_pendingSends.TryDequeue(out send)) DecrementCount(); while (_pendingEdits.TryDequeue(out edit)) DecrementCount(); - while (_pendingDeletes.TryDequeue(out msg)) + while (_pendingDeletes.TryDequeue(out delete)) DecrementCount(); - _pendingSendsByNonce.Clear(); - } - - private int GenerateNonce() - { - lock (_nonceRand) - return _nonceRand.Next(1, int.MaxValue); } } } diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index 306122ba3..6f53fae19 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -1,23 +1,17 @@ -using System; +#pragma warning disable CA1032, CA2237 +using System; using System.Net; namespace Discord.Net { -#if NET46 - [Serializable] -#endif - public class HttpException : Exception - { - public HttpStatusCode StatusCode { get; } - - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {(int)statusCode} ({statusCode})") - { - StatusCode = statusCode; + public class HttpException : Exception + { + public HttpStatusCode StatusCode { get; } + + public HttpException(HttpStatusCode statusCode) + : base($"The server responded with error {(int)statusCode} ({statusCode})") + { + StatusCode = statusCode; } -#if NET46 - public override void GetObjectData(SerializationInfo info, StreamingContext context) - => base.GetObjectData(info, context); -#endif } } diff --git a/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs b/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs new file mode 100644 index 000000000..4299df7bf --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class ChannelTypeConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ChannelType); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "text": + return ChannelType.Text; + case "voice": + return ChannelType.Voice; + default: + throw new JsonSerializationException("Unknown channel type"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((ChannelType)value) + { + case ChannelType.Text: + writer.WriteValue("text"); + break; + case ChannelType.Voice: + writer.WriteValue("voice"); + break; + default: + throw new JsonSerializationException("Invalid channel type"); + } + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/ImageConverter.cs b/src/Discord.Net/Net/JsonConverters/ImageConverter.cs new file mode 100644 index 000000000..bdbfa6915 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/ImageConverter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; +using System.IO; + +namespace Discord.Net.JsonConverters +{ + public class ImageConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(Stream); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var stream = value as Stream; + + byte[] bytes = new byte[stream.Length - stream.Position]; + stream.Read(bytes, 0, bytes.Length); + + string base64 = Convert.ToBase64String(bytes); + writer.WriteValue($"data:image/jpeg;base64,{base64}"); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs b/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs new file mode 100644 index 000000000..33c7c8ce7 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + public class NullableUInt64Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ulong?); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + object value = reader.Value; + if (value != null) + return ulong.Parse((string)value, NumberStyles.None, CultureInfo.InvariantCulture); + else + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue(((ulong?)value).Value.ToString(CultureInfo.InvariantCulture)); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs b/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs new file mode 100644 index 000000000..21a8ed8db --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class PermissionTargetConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(PermissionTarget); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "member": + return PermissionTarget.User; + case "role": + return PermissionTarget.Role; + default: + throw new JsonSerializationException("Unknown permission target"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((PermissionTarget)value) + { + case PermissionTarget.User: + writer.WriteValue("member"); + break; + case PermissionTarget.Role: + writer.WriteValue("role"); + break; + default: + throw new JsonSerializationException("Invalid permission target"); + } + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs b/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs new file mode 100644 index 000000000..b19b5977b --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class StringEntityConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs b/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs new file mode 100644 index 000000000..4b03f6fa3 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + internal class UInt64ArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = new List(); + if (reader.TokenType == JsonToken.StartArray) + { + reader.Read(); + while (reader.TokenType != JsonToken.EndArray) + { + ulong id = ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + result.Add(id); + reader.Read(); + } + } + return result.ToArray(); + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + writer.WriteStartArray(); + var a = (ulong[])value; + for (int i = 0; i < a.Length; i++) + writer.WriteValue(a[i].ToString(CultureInfo.InvariantCulture)); + writer.WriteEndArray(); + } + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs b/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs new file mode 100644 index 000000000..49c21564a --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + public class UInt64Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ulong); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(((ulong)value).ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs b/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs new file mode 100644 index 000000000..84945a672 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class UInt64EntityConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs b/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs new file mode 100644 index 000000000..a60d75f75 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class UserStatusConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(UserStatus); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "online": + return UserStatus.Online; + case "idle": + return UserStatus.Idle; + case "offline": + return UserStatus.Offline; + default: + throw new JsonSerializationException("Unknown user status"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((UserStatus)value) + { + case UserStatus.Online: + writer.WriteValue("online"); + break; + case UserStatus.Idle: + writer.WriteValue("idle"); + break; + case UserStatus.Offline: + writer.WriteValue("offline"); + break; + default: + throw new JsonSerializationException("Invalid user status"); + } + } + } +} diff --git a/src/Discord.Net/Net/Rest/BuiltInEngine.cs b/src/Discord.Net/Net/Rest/BuiltInEngine.cs deleted file mode 100644 index 83d12ec50..000000000 --- a/src/Discord.Net/Net/Rest/BuiltInEngine.cs +++ /dev/null @@ -1,145 +0,0 @@ -#if DOTNET5_4 -using Discord.Logging; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Net.Http; -using System.Net; -using System.Text; -using System.Globalization; -using Nito.AsyncEx; - -namespace Discord.Net.Rest -{ - internal class BuiltInEngine : IRestEngine - { - private const int HR_SECURECHANNELFAILED = -2146233079; - - private readonly DiscordConfig _config; - private readonly HttpClient _client; - private readonly string _baseUrl; - - private readonly AsyncLock _rateLimitLock; - private readonly ILogger _logger; - private DateTime _rateLimitTime; - - - public BuiltInEngine(DiscordConfig config, string baseUrl, ILogger logger) - { - _config = config; - _baseUrl = baseUrl; - _logger = logger; - - _rateLimitLock = new AsyncLock(); - _client = new HttpClient(new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - UseCookies = false, - UseProxy = false, - PreAuthenticate = false //We do auth ourselves - }); - _client.DefaultRequestHeaders.Add("accept", "*/*"); - _client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate"); - _client.DefaultRequestHeaders.Add("user-agent", config.UserAgent); - } - - public void SetToken(string token) - { - _client.DefaultRequestHeaders.Remove("authorization"); - if (token != null) - _client.DefaultRequestHeaders.Add("authorization", token); - } - - public async Task Send(string method, string path, string json, CancellationToken cancelToken) - { - using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) - { - if (json != null) - request.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await Send(request, cancelToken).ConfigureAwait(false); - } - } - public async Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) - { - using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) - { - var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); - content.Add(new StreamContent(File.OpenRead(path)), "file", filename); - request.Content = content; - return await Send(request, cancelToken).ConfigureAwait(false); - } - } - private async Task Send(HttpRequestMessage request, CancellationToken cancelToken) - { - int retryCount = 0; - while (true) - { - HttpResponseMessage response; - try - { - response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - } - catch (WebException ex) - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw; - } - - int statusCode = (int)response.StatusCode; - if (statusCode == 429) //Rate limit - { - var retryAfter = response.Headers - .Where(x => x.Key.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Value.FirstOrDefault()) - .FirstOrDefault(); - - int milliseconds; - if (retryAfter != null && int.TryParse(retryAfter, out milliseconds)) - { - if (_logger != null) - { - var now = DateTime.UtcNow; - if (now >= _rateLimitTime) - { - using (await _rateLimitLock.LockAsync().ConfigureAwait(false)) - { - if (now >= _rateLimitTime) - { - _rateLimitTime = now.AddMilliseconds(milliseconds); - _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); - } - } - } - } - await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); - continue; - } - throw new HttpException(response.StatusCode); - } - else if (statusCode < 200 || statusCode >= 300) //2xx = Success - throw new HttpException(response.StatusCode); - else - return await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - } - - private static readonly HttpMethod _patch = new HttpMethod("PATCH"); - private HttpMethod GetMethod(string method) - { - switch (method) - { - case "DELETE": return HttpMethod.Delete; - case "GET": return HttpMethod.Get; - case "PATCH": return _patch; - case "POST": return HttpMethod.Post; - case "PUT": return HttpMethod.Put; - default: throw new InvalidOperationException($"Unknown HttpMethod: {method}"); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs b/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs deleted file mode 100644 index 1bee431b0..000000000 --- a/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Discord.API; - -namespace Discord.Net.Rest -{ - public class CompletedRequestEventArgs : RequestEventArgs - { - public object Response { get; set; } - public string ResponseJson { get; set; } - public double Milliseconds { get; set; } - - public CompletedRequestEventArgs(IRestRequest request, object response, string responseJson, double milliseconds) - : base(request) - { - Response = response; - ResponseJson = responseJson; - Milliseconds = milliseconds; - } - } -} diff --git a/src/Discord.Net/Net/Rest/DefaultRestEngine.cs b/src/Discord.Net/Net/Rest/DefaultRestEngine.cs new file mode 100644 index 000000000..84ddfbbeb --- /dev/null +++ b/src/Discord.Net/Net/Rest/DefaultRestEngine.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + public class DefaultRestEngine : IRestEngine + { + private const int HR_SECURECHANNELFAILED = -2146233079; + + protected readonly HttpClient _client; + protected readonly string _baseUrl; + protected readonly CancellationToken _cancelToken; + protected bool _isDisposed; + + public DefaultRestEngine(string baseUrl, CancellationToken cancelToken) + { + _baseUrl = baseUrl; + _cancelToken = cancelToken; + + _client = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + UseCookies = false, + UseProxy = false, + PreAuthenticate = false + }); + SetHeader("accept-encoding", "gzip,deflate"); + } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + _client.DefaultRequestHeaders.Add(key, value); + } + + public async Task Send(IRestRequest request) + { + string uri = Path.Combine(_baseUrl, request.Endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(request.Method), uri)) + { + object payload = request.Payload; + if (payload != null) + restRequest.Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); + return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + } + } + + public async Task Send(IRestFileRequest request) + { + string uri = Path.Combine(_baseUrl, request.Endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(request.Method), uri)) + { + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + var mpParameters = request.MultipartParameters; + if (mpParameters != null) + { + foreach (var p in mpParameters) + content.Add(new StringContent(p.Value), p.Key); + + } + content.Add(new StreamContent(request.Stream), "file", request.Filename); + restRequest.Content = content; + return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + } + } + + private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken) + { + int retryCount = 0; + while (true) + { + HttpResponseMessage response; + try + { + response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + } + catch (WebException ex) + { + //The request was aborted: Could not create SSL/TLS secure channel. + if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) + continue; //Retrying seems to fix this somehow? + throw; + } + + int statusCode = (int)response.StatusCode; + if (statusCode < 200 || statusCode >= 300) //2xx = Success + throw new HttpException(response.StatusCode); + + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + } + + private static readonly HttpMethod _patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": return HttpMethod.Delete; + case "GET": return HttpMethod.Get; + case "PATCH": return _patch; + case "POST": return HttpMethod.Post; + case "PUT": return HttpMethod.Put; + default: throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + } +} diff --git a/src/Discord.Net/Net/Rest/ETFRestClient.cs b/src/Discord.Net/Net/Rest/ETFRestClient.cs deleted file mode 100644 index 5a2620e89..000000000 --- a/src/Discord.Net/Net/Rest/ETFRestClient.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Discord.ETF; -using System.IO; -using System; -using Discord.Logging; - -namespace Discord.Net.Rest -{ - public class ETFRestClient : RestClient - { - private readonly ETFWriter _serializer; - - public ETFRestClient(DiscordConfig config, string baseUrl, ILogger logger = null) - : base(config, baseUrl, logger) - { - _serializer = new ETFWriter(new MemoryStream()); - } - - protected override string Serialize(T obj) - { - throw new NotImplementedException(); - } - protected override T Deserialize(string json) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Discord.Net/Net/Rest/IRestEngine.cs b/src/Discord.Net/Net/Rest/IRestEngine.cs index faba37086..e086d79a0 100644 --- a/src/Discord.Net/Net/Rest/IRestEngine.cs +++ b/src/Discord.Net/Net/Rest/IRestEngine.cs @@ -1,13 +1,14 @@ -using System.IO; -using System.Threading; +using System; +using System.IO; using System.Threading.Tasks; namespace Discord.Net.Rest { - internal interface IRestEngine - { - void SetToken(string token); - Task Send(string method, string path, string json, CancellationToken cancelToken); - Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken); - } + public interface IRestEngine : IDisposable + { + void SetHeader(string key, string value); + + Task Send(IRestRequest request); + Task Send(IRestFileRequest request); + } } diff --git a/ref/Net/Rest/IRestRequest.cs b/src/Discord.Net/Net/Rest/IRestRequest.cs similarity index 53% rename from ref/Net/Rest/IRestRequest.cs rename to src/Discord.Net/Net/Rest/IRestRequest.cs index 9d46e645f..0ec7b9a32 100644 --- a/ref/Net/Rest/IRestRequest.cs +++ b/src/Discord.Net/Net/Rest/IRestRequest.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; namespace Discord.Net.Rest { @@ -8,8 +9,8 @@ namespace Discord.Net.Rest string Endpoint { get; } object Payload { get; } } - public interface IRestRequest : IRestRequest - where ResponseT : class + public interface IRestRequest : IRestRequest + where TResponse : class { } @@ -17,9 +18,10 @@ namespace Discord.Net.Rest { string Filename { get; } Stream Stream { get; } + IReadOnlyList MultipartParameters { get; } } - public interface IRestFileRequest : IRestFileRequest, IRestRequest - where ResponseT : class + public interface IRestFileRequest : IRestFileRequest, IRestRequest + where TResponse : class { } } diff --git a/src/Discord.Net/Net/Rest/JsonRestClient.cs b/src/Discord.Net/Net/Rest/JsonRestClient.cs deleted file mode 100644 index ac18ac823..000000000 --- a/src/Discord.Net/Net/Rest/JsonRestClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Discord.Logging; -using Newtonsoft.Json; -#if TEST_RESPONSES -using System; -#endif -using System.IO; - -namespace Discord.Net.Rest -{ - public class JsonRestClient : RestClient - { - private JsonSerializer _serializer; - - public JsonRestClient(DiscordConfig config, string baseUrl, ILogger logger = null) - : base(config, baseUrl, logger) - { - _serializer = new JsonSerializer(); -#if TEST_RESPONSES - _serializer.CheckAdditionalContent = true; - _serializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - _serializer.CheckAdditionalContent = false; - _serializer.MissingMemberHandling = MissingMemberHandling.Ignore; -#endif - } - - protected override string Serialize(T obj) - { - return JsonConvert.SerializeObject(obj); - } - - protected override T Deserialize(string json) - { -#if TEST_RESPONSES - if (string.IsNullOrEmpty(json)) - throw new Exception("API check failed: Response is empty."); -#endif - using (var reader = new JsonTextReader(new StringReader(json))) - return (T)_serializer.Deserialize(reader, typeof(T)); - } - } -} diff --git a/src/Discord.Net/Net/Rest/RequestEventArgs.cs b/src/Discord.Net/Net/Rest/RequestEventArgs.cs deleted file mode 100644 index ce6dadd83..000000000 --- a/src/Discord.Net/Net/Rest/RequestEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Discord.API; -using System; - -namespace Discord.Net.Rest -{ - public class RequestEventArgs : EventArgs - { - public IRestRequest Request { get; set; } - public bool Cancel { get; set; } - - public RequestEventArgs(IRestRequest request) - { - Request = request; - } - } -} diff --git a/src/Discord.Net/Net/Rest/RestClient.cs b/src/Discord.Net/Net/Rest/RestClient.cs index 47853f8a4..592a99d3d 100644 --- a/src/Discord.Net/Net/Rest/RestClient.cs +++ b/src/Discord.Net/Net/Rest/RestClient.cs @@ -1,140 +1,98 @@ -using Discord.API; -using Discord.ETF; -using Discord.Logging; +using Discord.Net.JsonConverters; +using Newtonsoft.Json; using System; using System.Diagnostics; -using System.Threading; +using System.IO; using System.Threading.Tasks; namespace Discord.Net.Rest { - public abstract partial class RestClient - { - private struct RestResults - { - public string Response { get; set; } - public double Milliseconds { get; set; } - - public RestResults(string response, double milliseconds) - { - Response = response; - Milliseconds = milliseconds; - } - } - - public event EventHandler SendingRequest = delegate { }; - public event EventHandler SentRequest = delegate { }; + public class RestClient + { + internal event EventHandler SentRequest; + + private readonly IRestEngine _engine; + private readonly JsonSerializer _serializer; - private bool OnSendingRequest(IRestRequest request) + internal RestClient(IRestEngine engine) { - var eventArgs = new RequestEventArgs(request); - SendingRequest(this, eventArgs); - return !eventArgs.Cancel; + _engine = engine; + _serializer = new JsonSerializer(); + _serializer.Converters.Add(new ChannelTypeConverter()); + _serializer.Converters.Add(new ImageConverter()); + _serializer.Converters.Add(new NullableUInt64Converter()); + _serializer.Converters.Add(new PermissionTargetConverter()); + _serializer.Converters.Add(new StringEntityConverter()); + _serializer.Converters.Add(new UInt64ArrayConverter()); + _serializer.Converters.Add(new UInt64Converter()); + _serializer.Converters.Add(new UInt64EntityConverter()); + _serializer.Converters.Add(new UserStatusConverter()); } - private void OnSentRequest(IRestRequest request, object response, string responseJson, double milliseconds) - => SentRequest(this, new CompletedRequestEventArgs(request, response, responseJson, milliseconds)); - - private readonly DiscordConfig _config; - private readonly IRestEngine _engine; - private readonly ETFWriter _serializer; - private readonly ILogger _logger; - private string _token; + public void Dispose() => _engine.Dispose(); - public CancellationToken CancelToken { get; set; } + public void SetHeader(string key, string value) => _engine.SetHeader(key, value); - public string Token + public async Task Send(IRestRequest request) + where TResponse : class { - get { return _token; } - set - { - _token = value; - _engine.SetToken(value); - } - } - - protected RestClient(DiscordConfig config, string baseUrl, ILogger logger = null) - { - _config = config; - _logger = logger; - -#if !DOTNET5_4 - _engine = new RestSharpEngine(config, baseUrl, logger); -#else - _engine = new BuiltInEngine(config, baseUrl, logger); -#endif - - if (logger != null && logger.Level >= LogSeverity.Verbose) - SentRequest += (s, e) => _logger.Verbose($"{e.Request.Method} {e.Request.Endpoint}: {e.Milliseconds} ms"); - } - - public async Task Send(IRestRequest request) - where ResponseT : class - { if (request == null) throw new ArgumentNullException(nameof(request)); + + var stopwatch = Stopwatch.StartNew(); + Stream response = await _engine.Send(request).ConfigureAwait(false); + TResponse responseObj = Deserialize(response); + stopwatch.Stop(); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await Send(request, true).ConfigureAwait(false); - var response = Deserialize(results.Response); - OnSentRequest(request, response, results.Response, results.Milliseconds); - - return response; + SentRequest(this, new SentRequestEventArgs(request, responseObj, ToMilliseconds(stopwatch))); + return responseObj; } public async Task Send(IRestRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await Send(request, false).ConfigureAwait(false); - OnSentRequest(request, null, null, results.Milliseconds); + var stopwatch = Stopwatch.StartNew(); + await _engine.Send(request).ConfigureAwait(false); + stopwatch.Stop(); + + SentRequest(this, new SentRequestEventArgs(request, null, ToMilliseconds(stopwatch))); } - public async Task Send(IRestFileRequest request) - where ResponseT : class + public async Task Send(IRestFileRequest request) + where TResponse : class { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await SendFile(request, true).ConfigureAwait(false); - var response = Deserialize(results.Response); - OnSentRequest(request, response, results.Response, results.Milliseconds); + var stopwatch = Stopwatch.StartNew(); + Stream response = await _engine.Send(request).ConfigureAwait(false); + TResponse responseObj = Deserialize(response); + stopwatch.Stop(); - return response; + SentRequest(this, new SentRequestEventArgs(request, responseObj, ToMilliseconds(stopwatch))); + return responseObj; } public async Task Send(IRestFileRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await SendFile(request, false).ConfigureAwait(false); - OnSentRequest(request, null, null, results.Milliseconds); - } - - private async Task Send(IRestRequest request, bool hasResponse) - { - object payload = request.Payload; - string requestJson = null; - if (payload != null) - requestJson = Serialize(payload); - - Stopwatch stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.Send(request.Method, request.Endpoint, requestJson, CancelToken).ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); + await _engine.Send(request).ConfigureAwait(false); stopwatch.Stop(); - double milliseconds = Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - return new RestResults(responseJson, milliseconds); - } - - private async Task SendFile(IRestFileRequest request, bool hasResponse) - { - Stopwatch stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.SendFile(request.Method, request.Endpoint, request.Filename, request.Stream, CancelToken).ConfigureAwait(false); - stopwatch.Stop(); + SentRequest(this, new SentRequestEventArgs(request, null, ToMilliseconds(stopwatch))); + } - double milliseconds = Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - return new RestResults(responseJson, milliseconds); + private void Serialize(Stream stream, T value) + { + using (TextWriter text = new StreamWriter(stream)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value, typeof(T)); + } + private T Deserialize(Stream stream) + { + using (TextReader text = new StreamReader(stream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); } - protected abstract string Serialize(T obj); - protected abstract T Deserialize(string json); - } + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + } } diff --git a/src/Discord.Net/Net/Rest/RestClientProvider.cs b/src/Discord.Net/Net/Rest/RestClientProvider.cs new file mode 100644 index 000000000..942720ffb --- /dev/null +++ b/src/Discord.Net/Net/Rest/RestClientProvider.cs @@ -0,0 +1,6 @@ +using System.Threading; + +namespace Discord.Net.Rest +{ + public delegate IRestEngine RestClientProvider(string baseUrl, CancellationToken cancelToken); +} diff --git a/src/Discord.Net/Net/Rest/RestParameter.cs b/src/Discord.Net/Net/Rest/RestParameter.cs new file mode 100644 index 000000000..5fac47bf8 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RestParameter.cs @@ -0,0 +1,19 @@ +namespace Discord.Net.Rest +{ + public struct RestParameter + { + public string Key { get; } + public string Value { get; } + + public RestParameter(string key, string value) + { + Key = key; + Value = value; + } + public RestParameter(string key, object value) + { + Key = key; + Value = value.ToString(); + } + } +} diff --git a/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs b/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs new file mode 100644 index 000000000..25f433d25 --- /dev/null +++ b/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs @@ -0,0 +1,16 @@ +namespace Discord.Net.Rest +{ + public class SentRequestEventArgs + { + public IRestRequest Request { get; } + public object Response { get; } + public double Milliseconds { get; } + + public SentRequestEventArgs(IRestRequest request, object response, double milliseconds) + { + Request = request; + Response = response; + Milliseconds = milliseconds; + } + } +} diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs deleted file mode 100644 index f9c54064b..000000000 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ /dev/null @@ -1,132 +0,0 @@ -#if !DOTNET5_4 -using Discord.Logging; -using Nito.AsyncEx; -using RestSharp; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using RestSharpClient = RestSharp.RestClient; - -namespace Discord.Net.Rest -{ - internal class RestSharpEngine : IRestEngine - { - private const int HR_SECURECHANNELFAILED = -2146233079; - - private readonly DiscordConfig _config; - private readonly RestSharpClient _client; - - private readonly AsyncLock _rateLimitLock; - private readonly ILogger _logger; - private DateTime _rateLimitTime; - - public RestSharpEngine(DiscordConfig config, string baseUrl, ILogger logger) - { - _config = config; - _logger = logger; - - _rateLimitLock = new AsyncLock(); - _client = new RestSharpClient(baseUrl) - { - PreAuthenticate = false, - ReadWriteTimeout = DiscordConfig.RestTimeout, - UserAgent = config.UserAgent - }; - _client.Proxy = null; - _client.RemoveDefaultParameter("Accept"); - _client.AddDefaultHeader("accept", "*/*"); - _client.AddDefaultHeader("accept-encoding", "gzip,deflate"); - } - - public void SetToken(string token) - { - _client.RemoveDefaultParameter("authorization"); - if (token != null) - _client.AddDefaultHeader("authorization", token); - } - - public Task Send(string method, string path, string json, CancellationToken cancelToken) - { - var request = new RestRequest(path, GetMethod(method)); - if (json != null) - request.AddParameter("application/json", json, ParameterType.RequestBody); - return Send(request, cancelToken); - } - public Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) - { - var request = new RestRequest(path, GetMethod(method)); - request.AddHeader("content-length", (stream.Length - stream.Position).ToString()); - - byte[] bytes = new byte[stream.Length - stream.Position]; - stream.Read(bytes, 0, bytes.Length); - request.AddFileBytes("file", bytes, filename); - //request.AddFile("file", x => stream.CopyTo(x), filename); (Broken in latest ver) - - return Send(request, cancelToken); - } - private async Task Send(RestRequest request, CancellationToken cancelToken) - { - int retryCount = 0; - while (true) - { - var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false); - int statusCode = (int)response.StatusCode; - if (statusCode == 0) //Internal Error - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (response.ErrorException.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw response.ErrorException; - } - else if (statusCode == 429) //Rate limit - { - var retryAfter = response.Headers - .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); - - int milliseconds; - if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) - { - if (_logger != null) - { - var now = DateTime.UtcNow; - if (now >= _rateLimitTime) - { - using (await _rateLimitLock.LockAsync().ConfigureAwait(false)) - { - if (now >= _rateLimitTime) - { - _rateLimitTime = now.AddMilliseconds(milliseconds); - _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); - } - } - } - } - await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); - continue; - } - throw new HttpException(response.StatusCode); - } - else if (statusCode < 200 || statusCode >= 300) //2xx = Success - throw new HttpException(response.StatusCode); - else - return response.Content; - } - } - - private Method GetMethod(string method) - { - switch (method) - { - case "DELETE": return Method.DELETE; - case "GET": return Method.GET; - case "PATCH": return Method.PATCH; - case "POST": return Method.POST; - case "PUT": return Method.PUT; - default: throw new InvalidOperationException($"Unknown HttpMethod: {method}"); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/TimeoutException.cs b/src/Discord.Net/Net/TimeoutException.cs deleted file mode 100644 index 051eeb263..000000000 --- a/src/Discord.Net/Net/TimeoutException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord.Net -{ -#if NET46 - [Serializable] -#endif - public class TimeoutException : OperationCanceledException - { - public TimeoutException() - : base("An operation has timed out.") - { - } - } -} diff --git a/src/Discord.Net/Net/WebSocketException.cs b/src/Discord.Net/Net/WebSocketException.cs deleted file mode 100644 index b845d90c4..000000000 --- a/src/Discord.Net/Net/WebSocketException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class WebSocketException : Exception - { - public int Code { get; } - public string Reason { get; } - - public WebSocketException(int code, string reason) - : base(GenerateMessage(code, reason)) - { - Code = code; - Reason = reason; - } - - private static string GenerateMessage(int? code, string reason) - { - if (!String.IsNullOrEmpty(reason)) - return $"Received close code {code}: {reason}"; - else - return $"Received close code {code}"; - } - } -} diff --git a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs index 7c6f633e9..3fd4425fa 100644 --- a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs +++ b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs @@ -6,6 +6,6 @@ namespace Discord.Net.WebSockets { public byte[] Data { get; } - public BinaryMessageEventArgs(byte[] data) { Data = data; } + public BinaryMessageEventArgs(byte[] data) { } } } diff --git a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs b/src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs similarity index 59% rename from src/Discord.Net/Net/WebSockets/BuiltInEngine.cs rename to src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs index bda5ccb15..d9486c751 100644 --- a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs @@ -1,66 +1,87 @@ -#if DOTNET5_4 -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using WebSocketClient = System.Net.WebSockets.ClientWebSocket; namespace Discord.Net.WebSockets { - internal class BuiltInEngine : IWebSocketEngine + public class DefaultWebSocketEngine : IWebSocketEngine { - private const int ReceiveChunkSize = 12 * 1024; //12KB - private const int SendChunkSize = 4 * 1024; //4KB - private const int HR_TIMEOUT = -2147012894; - - private readonly DiscordConfig _config; - private readonly ConcurrentQueue _sendQueue; - private WebSocketClient _webSocket; - private Task _tempTask; + public const int ReceiveChunkSize = 12 * 1024; //12KB + public const int SendChunkSize = 4 * 1024; //4KB + protected const int HR_TIMEOUT = -2147012894; public event EventHandler BinaryMessage = delegate { }; public event EventHandler TextMessage = delegate { }; - private void OnBinaryMessage(byte[] data) - => BinaryMessage(this, new BinaryMessageEventArgs(data)); - private void OnTextMessage(string msg) - => TextMessage(this, new TextMessageEventArgs(msg)); - internal BuiltInEngine(DiscordConfig config) + protected readonly ConcurrentQueue _sendQueue; + protected readonly ClientWebSocket _client; + protected Task _receiveTask, _sendTask; + protected CancellationTokenSource _cancelToken; + protected bool _isDisposed; + + public DefaultWebSocketEngine() { - _config = config; _sendQueue = new ConcurrentQueue(); + + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); } public async Task Connect(string host, CancellationToken cancelToken) { - _webSocket = new WebSocketClient(); - _webSocket.Options.Proxy = null; - _webSocket.Options.SetRequestHeader("User-Agent", _config.UserAgent); - _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; - _tempTask = await _webSocket.ConnectAsync(new Uri(host), cancelToken)//.ConfigureAwait(false); - .ContinueWith(t => ReceiveAsync(cancelToken)).ConfigureAwait(false); - //TODO: ContinueWith is a temporary hack, may be a bug related to https://github.com/dotnet/corefx/issues/4429 - } + await Disconnect().ConfigureAwait(false); - public Task Disconnect() + _cancelToken = new CancellationTokenSource(); + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken.Token, cancelToken).Token; + + await _client.ConnectAsync(new Uri(host), combinedToken).ConfigureAwait(false); + _receiveTask = TaskHelper.CreateLongRunning(() => ReceiveAsync(combinedToken), combinedToken); + _sendTask = TaskHelper.CreateLongRunning(() => SendAsync(combinedToken), combinedToken); + } + public async Task Disconnect() { + _cancelToken.Cancel(); + string ignored; while (_sendQueue.TryDequeue(out ignored)) { } - var socket = _webSocket; - _webSocket = null; + _client.Abort(); - return TaskHelper.CompletedTask; + var receiveTask = _receiveTask ?? TaskHelper.CompletedTask; + var sendTask = _sendTask ?? TaskHelper.CompletedTask; + await Task.WhenAll(receiveTask, sendTask).ConfigureAwait(false); } - public IEnumerable GetTasks(CancellationToken cancelToken) - => new Task[] { /*ReceiveAsync(cancelToken),*/ _tempTask, SendAsync(cancelToken) }; + public void SetHeader(string key, string value) + { + _client.Options.SetRequestHeader(key, value); + } + + public void QueueMessage(string message) + { + _sendQueue.Enqueue(message); + } + //TODO: Check this code private Task ReceiveAsync(CancellationToken cancelToken) { return Task.Run(async () => @@ -79,7 +100,7 @@ namespace Discord.Net.WebSockets try { - result = await _webSocket.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { @@ -96,9 +117,12 @@ namespace Discord.Net.WebSockets var array = stream.ToArray(); if (result.MessageType == WebSocketMessageType.Binary) - OnBinaryMessage(array); + BinaryMessage(this, new BinaryMessageEventArgs(array)); else if (result.MessageType == WebSocketMessageType.Text) - OnTextMessage(Encoding.UTF8.GetString(array, 0, array.Length)); + { + string text = Encoding.UTF8.GetString(array, 0, array.Length); + TextMessage(this, new TextMessageEventArgs(text)); + } stream.Position = 0; stream.SetLength(0); @@ -107,6 +131,8 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { } }); } + + //TODO: Check this code private Task SendAsync(CancellationToken cancelToken) { return Task.Run(async () => @@ -124,7 +150,7 @@ namespace Discord.Net.WebSockets int frameCount = (int)Math.Ceiling((double)byteCount / SendChunkSize); int offset = 0; - for (var i = 0; i < frameCount; i++, offset += SendChunkSize) + for (int i = 0; i < frameCount; i++, offset += SendChunkSize) { bool isLast = i == (frameCount - 1); @@ -136,7 +162,7 @@ namespace Discord.Net.WebSockets try { - await _webSocket.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); + await _client.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { @@ -150,9 +176,5 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { } }); } - - public void QueueMessage(string message) - => _sendQueue.Enqueue(message); } } -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs index 4afe5aae7..3e841de89 100644 --- a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -1,14 +1,5 @@ -using Discord.API.Client; -using Discord.API.Client.GatewaySocket; -using Discord.API.Client.Rest; -using Discord.Logging; -using Discord.Net.Rest; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,175 +7,36 @@ namespace Discord.Net.WebSockets { public class GatewaySocket : WebSocket { - private RestClient _rest; - private uint _lastSequence; - private int _reconnects; - - //public string Token { get; private set; } - public string SessionId { get; private set; } - public event EventHandler ReceivedDispatch = delegate { }; - private void OnReceivedDispatch(string type, JToken payload) - => ReceivedDispatch(this, new WebSocketEventEventArgs(type, payload)); - - public GatewaySocket(DiscordConfig config, JsonSerializer serializer, Logger logger) - : base(config, serializer, logger) - { - Disconnected += async (s, e) => - { - if (e.WasUnexpected) - await Reconnect().ConfigureAwait(false); - }; - } - - public async Task Connect(RestClient rest, CancellationToken parentCancelToken) - { - _rest = rest; - //Token = rest.Token; - - var gatewayResponse = await rest.Send(new GatewayRequest()).ConfigureAwait(false); - Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}"); - Host = gatewayResponse.Url; - await BeginConnect(parentCancelToken).ConfigureAwait(false); - if (SessionId == null) - SendIdentify(_rest.Token); - else - SendResume(); - } - private async Task Reconnect() - { - try - { - var cancelToken = _parentCancelToken; - if (_reconnects++ == 0) - await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); - else - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + public ConnectionState State { get; } + public string Host { get; } + public string SessionId { get; } - while (!cancelToken.IsCancellationRequested) - { - try - { - await Connect(_rest, _parentCancelToken).ConfigureAwait(false); - break; - } - catch (OperationCanceledException) { throw; } - catch (Exception ex) - { - Logger.Error("Reconnect failed", ex); - //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException) { } - } - public async Task Disconnect() + internal GatewaySocket(IWebSocketEngine engine) + : base(engine) { - await _taskManager.Stop(true).ConfigureAwait(false); - //Token = null; - SessionId = null; } - protected override async Task Run() + public void SetHeader(string key, string value) => _engine.SetHeader(key, value); + + internal Task Connect(CancellationToken cancelToken) { - List tasks = new List(); - tasks.AddRange(_engine.GetTasks(CancelToken)); - tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); + return Task.Delay(0); } - protected override Task Cleanup() + internal Task Disconnect() { - var ex = _taskManager.Exception; - if (ex == null || (ex as WebSocketException)?.Code != 1012) //if (ex == null || (ex as WebSocketException)?.Code != 1012) - SessionId = null; //Reset session unless close code 1012 - return base.Cleanup(); + return Task.Delay(0); } - protected override async Task ProcessMessage(string json) - { - base.ProcessMessage(json).GetAwaiter().GetResult(); //This is just a CompletedTask, and we need to avoid asyncs in here + public void SendIdentify(string token) { } - WebSocketMessage msg; - using (var reader = new JsonTextReader(new StringReader(json))) - msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage; - - if (msg.Sequence.HasValue) - _lastSequence = msg.Sequence.Value; - - var opCode = (OpCodes?)msg.Operation; - switch (opCode) - { - case OpCodes.Dispatch: - { - if (msg.Type == "READY") - SessionId = (msg.Payload as JToken).Value("session_id"); - - OnReceivedDispatch(msg.Type, msg.Payload as JToken); - - if (msg.Type == "READY" || msg.Type == "RESUMED") - { - _heartbeatInterval = (msg.Payload as JToken).Value("heartbeat_interval"); - _reconnects = 0; - await EndConnect().ConfigureAwait(false); //Complete the connect - } - } - break; - case OpCodes.Redirect: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - if (payload.Url != null) - { - Host = payload.Url; - Logger.Info("Redirected to " + payload.Url); - await Reconnect().ConfigureAwait(false); - } - } - break; - default: - if (opCode != null) - Logger.Warning($"Unknown Opcode: {opCode}"); - else - Logger.Warning($"Received message with no opcode"); - break; - } - } - - public void SendIdentify(string token) - { - var props = new Dictionary - { - ["$device"] = "Discord.Net" - }; - var msg = new IdentifyCommand() - { - Version = 3, - Token = token, - Properties = props, - LargeThreshold = _config.LargeThreshold, - UseCompression = true - }; - QueueMessage(msg); - } - - public void SendResume() - => QueueMessage(new ResumeCommand { SessionId = SessionId, Sequence = _lastSequence }); - public override void SendHeartbeat() - => QueueMessage(new HeartbeatCommand()); - public void SendUpdateStatus(long? idleSince, string gameName) - => QueueMessage(new UpdateStatusCommand - { - IdleSince = idleSince, - Game = gameName != null ? new UpdateStatusCommand.GameInfo { Name = gameName } : null - }); - public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) - => QueueMessage(new UpdateVoiceCommand { GuildId = serverId, ChannelId = channelId, IsSelfMuted = isSelfMuted, IsSelfDeafened = isSelfDeafened }); - public void SendRequestMembers(IEnumerable serverId, string query, int limit) - => QueueMessage(new RequestMembersCommand { GuildId = serverId.ToArray(), Query = query, Limit = limit }); - - //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached - public override void WaitForConnection(CancellationToken cancelToken) - => base.WaitForConnection(CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token); + public void SendResume() { } + public void SendHeartbeat() { } + public void SendUpdateStatus(long? idleSince, string gameName) { } + public void SendUpdateVoice(ulong? guildId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) { } + public void SendRequestMembers(IEnumerable guildId, string query, int limit) { } + + public void WaitForConnection(CancellationToken cancelToken) { } } } diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs b/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs index 68f31f12b..b43109276 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs @@ -1,18 +1,18 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public interface IWebSocketEngine - { - event EventHandler BinaryMessage; - event EventHandler TextMessage; + public interface IWebSocketEngine : IDisposable + { + event EventHandler BinaryMessage; + event EventHandler TextMessage; - Task Connect(string host, CancellationToken cancelToken); - Task Disconnect(); - void QueueMessage(string message); - IEnumerable GetTasks(CancellationToken cancelToken); - } + void SetHeader(string key, string value); + + Task Connect(string host, CancellationToken cancelToken); + Task Disconnect(); + void QueueMessage(string message); + } } diff --git a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs deleted file mode 100644 index 420299d6b..000000000 --- a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs +++ /dev/null @@ -1,141 +0,0 @@ -#if !DOTNET5_4 -using SuperSocket.ClientEngine; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using WebSocket4Net; -using WebSocketClient = WebSocket4Net.WebSocket; - -namespace Discord.Net.WebSockets -{ - internal class WS4NetEngine : IWebSocketEngine - { - private readonly DiscordConfig _config; - private readonly ConcurrentQueue _sendQueue; - private readonly TaskManager _taskManager; - private WebSocketClient _webSocket; - private ManualResetEventSlim _waitUntilConnect, _waitUntilDisconnect; - - public event EventHandler BinaryMessage = delegate { }; - public event EventHandler TextMessage = delegate { }; - private void OnBinaryMessage(byte[] data) - => BinaryMessage(this, new BinaryMessageEventArgs(data)); - private void OnTextMessage(string msg) - => TextMessage(this, new TextMessageEventArgs(msg)); - - internal WS4NetEngine(DiscordConfig config, TaskManager taskManager) - { - _config = config; - _taskManager = taskManager; - _sendQueue = new ConcurrentQueue(); - _waitUntilConnect = new ManualResetEventSlim(); - _waitUntilDisconnect = new ManualResetEventSlim(true); - } - - public Task Connect(string host, CancellationToken cancelToken) - { - try - { - _webSocket = new WebSocketClient(host); - _webSocket.EnableAutoSendPing = false; - _webSocket.NoDelay = true; - _webSocket.Proxy = null; - - _webSocket.DataReceived += OnWebSocketBinary; - _webSocket.MessageReceived += OnWebSocketText; - _webSocket.Error += OnWebSocketError; - _webSocket.Closed += OnWebSocketClosed; - _webSocket.Opened += OnWebSocketOpened; - - _waitUntilConnect.Reset(); - _waitUntilDisconnect.Reset(); - _webSocket.Open(); - _waitUntilConnect.Wait(cancelToken); - _taskManager.ThrowException(); //In case our connection failed - } - catch - { - _waitUntilDisconnect.Set(); - throw; - } - return TaskHelper.CompletedTask; - } - - public Task Disconnect() - { - string ignored; - while (_sendQueue.TryDequeue(out ignored)) { } - - var socket = _webSocket; - _webSocket = null; - if (socket != null) - { - socket.Close(); - socket.Opened -= OnWebSocketOpened; - socket.DataReceived -= OnWebSocketBinary; - socket.MessageReceived -= OnWebSocketText; - - _waitUntilDisconnect.Wait(); //We need the next two events to raise this one - socket.Error -= OnWebSocketError; - socket.Closed -= OnWebSocketClosed; - socket.Dispose(); - } - - return TaskHelper.CompletedTask; - } - - private async void OnWebSocketError(object sender, ErrorEventArgs e) - { - await _taskManager.SignalError(e.Exception).ConfigureAwait(false); - _waitUntilConnect.Set(); - _waitUntilDisconnect.Set(); - } - private async void OnWebSocketClosed(object sender, EventArgs e) - { - Exception ex; - if (e is ClosedEventArgs) - ex = new WebSocketException((e as ClosedEventArgs).Code, (e as ClosedEventArgs).Reason); - else - ex = new Exception("Connection lost"); - await _taskManager.SignalError(ex).ConfigureAwait(false); - _waitUntilConnect.Set(); - _waitUntilDisconnect.Set(); - } - private void OnWebSocketOpened(object sender, EventArgs e) - { - _waitUntilConnect.Set(); - _waitUntilDisconnect.Reset(); - } - private void OnWebSocketText(object sender, MessageReceivedEventArgs e) - => OnTextMessage(e.Message); - private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) - => OnBinaryMessage(e.Data); - - public IEnumerable GetTasks(CancellationToken cancelToken) - => new Task[] { SendAsync(cancelToken) }; - - private Task SendAsync(CancellationToken cancelToken) - { - return Task.Run(async () => - { - try - { - while (!cancelToken.IsCancellationRequested) - { - string json; - while (_sendQueue.TryDequeue(out json)) - _webSocket.Send(json); - await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) { } - }); - } - - public void QueueMessage(string message) - => _sendQueue.Enqueue(message); - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index aa2c9a98b..ed533883a 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -1,188 +1,27 @@ -using Discord.API.Client; -using Discord.Logging; -using Newtonsoft.Json; -using Nito.AsyncEx; -using System; -using System.IO; -using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; +using System; namespace Discord.Net.WebSockets { - public abstract partial class WebSocket - { - private readonly AsyncLock _lock; + public class WebSocket : IDisposable + { protected readonly IWebSocketEngine _engine; - protected readonly DiscordConfig _config; - protected readonly ManualResetEventSlim _connectedEvent; - protected readonly TaskManager _taskManager; - protected readonly JsonSerializer _serializer; - protected CancellationTokenSource _cancelSource; - protected CancellationToken _parentCancelToken; - protected int _heartbeatInterval; - private DateTime _lastHeartbeat; + protected bool _isDisposed; - /// Gets the logger used for this client. - protected internal Logger Logger { get; } - public CancellationToken CancelToken { get; private set; } - - public string Host { get; set; } - /// Gets the current connection state of this client. - public ConnectionState State { get; private set; } - - public event EventHandler Connected = delegate { }; - private void OnConnected() - => Connected(this, EventArgs.Empty); - public event EventHandler Disconnected = delegate { }; - private void OnDisconnected(bool wasUnexpected, Exception error) - => Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - - public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger) - { - _config = config; - _serializer = serializer; - Logger = logger; - - _lock = new AsyncLock(); - _taskManager = new TaskManager(Cleanup); - CancelToken = new CancellationToken(true); - _connectedEvent = new ManualResetEventSlim(false); - -#if !DOTNET5_4 - _engine = new WS4NetEngine(config, _taskManager); -#else - _engine = new BuiltInEngine(config); -#endif - _engine.BinaryMessage += (s, e) => - { - using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) - using (var decompressed = new MemoryStream()) - { - using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) - zlib.CopyTo(decompressed); - decompressed.Position = 0; - using (var reader = new StreamReader(decompressed)) - ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult(); - } - }; - _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); - } - - protected async Task BeginConnect(CancellationToken parentCancelToken) - { - try - { - using (await _lock.LockAsync().ConfigureAwait(false)) - { - _parentCancelToken = parentCancelToken; - - await _taskManager.Stop().ConfigureAwait(false); - _taskManager.ClearException(); - State = ConnectionState.Connecting; - - _cancelSource = new CancellationTokenSource(); - CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, parentCancelToken).Token; - _lastHeartbeat = DateTime.UtcNow; - - await _engine.Connect(Host, CancelToken).ConfigureAwait(false); - await Run().ConfigureAwait(false); - } - } - catch (Exception ex) - { - //TODO: Should this be inside the lock? - await _taskManager.SignalError(ex).ConfigureAwait(false); - throw; - } - } - protected async Task EndConnect() - { - try - { - State = ConnectionState.Connected; - Logger.Info($"Connected"); - - OnConnected(); - _connectedEvent.Set(); - } - catch (Exception ex) - { - await _taskManager.SignalError(ex).ConfigureAwait(false); - } - } - - protected abstract Task Run(); - protected virtual async Task Cleanup() - { - var oldState = State; - State = ConnectionState.Disconnecting; - - await _engine.Disconnect().ConfigureAwait(false); - _cancelSource = null; - _connectedEvent.Reset(); - - if (oldState == ConnectionState.Connecting || oldState == ConnectionState.Connected) - { - var ex = _taskManager.Exception; - if (ex == null) - Logger.Info("Disconnected"); - else - Logger.Error("Disconnected", ex); - State = ConnectionState.Disconnected; - OnDisconnected(!_taskManager.WasStopExpected, _taskManager.Exception); - } - else - State = ConnectionState.Disconnected; - } - - protected virtual Task ProcessMessage(string json) - { - return TaskHelper.CompletedTask; - } - protected void QueueMessage(IWebSocketMessage message) - { - string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); - _engine.QueueMessage(json); - } - - protected Task HeartbeatAsync(CancellationToken cancelToken) - { - return Task.Run(async () => - { - try - { - while (!cancelToken.IsCancellationRequested) - { - if (this.State == ConnectionState.Connected && _heartbeatInterval > 0) - { - SendHeartbeat(); - await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); - } - else - await Task.Delay(1000, cancelToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) { } - }); - } - public abstract void SendHeartbeat(); + internal WebSocket(IWebSocketEngine engine) + { + _engine = engine; + } - public virtual void WaitForConnection(CancellationToken cancelToken) + protected virtual void Dispose(bool disposing) { - try + if (!_isDisposed) { - if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken)) - { - if (State != ConnectionState.Connected) - throw new TimeoutException(); - } - } - catch (OperationCanceledException) - { - _taskManager.ThrowException(); //Throws data socket's internal error if any occured - throw; + if (disposing) + _engine.Dispose(); + + _isDisposed = true; } } - } + public void Dispose() => Dispose(true); + } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs b/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs index a0c60edcf..676c0ba6e 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs @@ -7,11 +7,5 @@ namespace Discord.Net.WebSockets { public string Type { get; } public JToken Payload { get; } - - internal WebSocketEventEventArgs(string type, JToken data) - { - Type = type; - Payload = data; - } } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs new file mode 100644 index 000000000..7e7652dbc --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs @@ -0,0 +1,6 @@ +using System.Threading; + +namespace Discord.Net.WebSockets +{ + public delegate IWebSocketEngine WebSocketProvider(CancellationToken cancelToken); +} diff --git a/src/Discord.Net/Properties/AssemblyInfo.cs b/src/Discord.Net/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b562e546b --- /dev/null +++ b/src/Discord.Net/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Discord.Net")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Discord.Net")] +[assembly: AssemblyCopyright("Copyright © Rogue Exception 2015-2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("c6a50d24-cbd3-4e76-852c-4dca60bbd608")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Discord.Net/ServiceCollection.cs b/src/Discord.Net/ServiceCollection.cs deleted file mode 100644 index 104f91dd4..000000000 --- a/src/Discord.Net/ServiceCollection.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Discord -{ - internal class ServiceCollection : IEnumerable - { - private readonly Dictionary _services; - - internal DiscordClient Client { get; } - - internal ServiceCollection(DiscordClient client) - { - Client = client; - _services = new Dictionary(); - } - - public T Add(T service) - where T : class, IService - { - _services.Add(typeof(T), service); - service.Install(Client); - return service; - } - - public T Get(bool isRequired = true) - where T : class, IService - { - IService service; - T singletonT = null; - - if (_services.TryGetValue(typeof(T), out service)) - singletonT = service as T; - - if (singletonT == null && isRequired) - throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}."); - return singletonT; - } - - public IEnumerator GetEnumerator() => _services.Values.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _services.Values.GetEnumerator(); - } -} diff --git a/src/Discord.Net/TaskHelper.cs b/src/Discord.Net/TaskHelper.cs new file mode 100644 index 000000000..17ecc4615 --- /dev/null +++ b/src/Discord.Net/TaskHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class TaskHelper + { +#if NETSTANDARD1_4 + public static Task CompletedTask => Task.CompletedTask; +#else + public static Task CompletedTask => Task.Delay(0); +#endif + + public static Task CreateLongRunning(Action action, CancellationToken cancelToken) + => Task.Factory.StartNew(action, cancelToken, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } +} diff --git a/src/Discord.Net/TaskManager.cs b/src/Discord.Net/TaskManager.cs deleted file mode 100644 index d43372396..000000000 --- a/src/Discord.Net/TaskManager.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.ExceptionServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - /// Helper class used to manage several tasks and keep them in sync. If any single task errors or stops, all other tasks will also be stopped. - public class TaskManager - { - private readonly AsyncLock _lock; - private readonly Func _stopAction; - private ExceptionDispatchInfo _stopReason; - - private CancellationTokenSource _cancelSource; - private Task _task; - - public bool StopOnCompletion { get; } - public bool WasStopExpected { get; private set; } - - public Exception Exception => _stopReason?.SourceException; - - internal TaskManager(bool stopOnCompletion) - { - _lock = new AsyncLock(); - StopOnCompletion = stopOnCompletion; - } - public TaskManager(Action stopAction, bool stopOnCompletion = true) - : this(stopOnCompletion) - { - _stopAction = TaskHelper.ToAsync(stopAction); - } - public TaskManager(Func stopAction, bool stopOnCompletion = true) - : this(stopOnCompletion) - { - _stopAction = stopAction; - } - - public async Task Start(IEnumerable tasks, CancellationTokenSource cancelSource) - { - if (tasks == null) throw new ArgumentNullException(nameof(tasks)); - if (cancelSource == null) throw new ArgumentNullException(nameof(cancelSource)); - - while (true) - { - var task = _task; - if (task != null) - await Stop().ConfigureAwait(false); - - using (await _lock.LockAsync().ConfigureAwait(false)) - { - _cancelSource = cancelSource; - - if (_task != null) - continue; //Another thread sneaked in and started this manager before we got a lock, loop and try again - - _stopReason = null; - WasStopExpected = false; - - Task[] tasksArray = tasks.ToArray(); - - _task = Task.Run(async () => - { - if (tasksArray.Length > 0) - { - Task anyTask = tasksArray.Length > 0 ? Task.WhenAny(tasksArray) : null; - Task allTasks = tasksArray.Length > 0 ? Task.WhenAll(tasksArray) : null; - //Wait for the first task to stop or error - Task firstTask = await anyTask.ConfigureAwait(false); - - //Signal the rest of the tasks to stop - if (firstTask.Exception != null) - await SignalError(firstTask.Exception).ConfigureAwait(false); - else if (StopOnCompletion) //Unless we allow for natural completions - await SignalStop().ConfigureAwait(false); - - //Wait for the other tasks, and signal their errors too just in case - try { await allTasks.ConfigureAwait(false); } - catch (AggregateException ex) { await SignalError(ex.InnerExceptions.First()).ConfigureAwait(false); } - catch (Exception ex) { await SignalError(ex).ConfigureAwait(false); } - } - - if (!StopOnCompletion && !_cancelSource.IsCancellationRequested) - { - try { await Task.Delay(-1, _cancelSource.Token).ConfigureAwait(false); } //Pause until TaskManager is stopped - catch (OperationCanceledException) { } - } - - //Run the cleanup function within our lock - if (_stopAction != null) - await _stopAction().ConfigureAwait(false); - _task = null; - _cancelSource = null; - }); - return; - } - } - } - - public async Task SignalStop(bool isExpected = false) - { - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (isExpected) - WasStopExpected = true; - - Cancel(); - } - } - public async Task Stop(bool isExpected = false) - { - Task task; - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (isExpected) - WasStopExpected = true; - - //Cache the task so we still have something to await if Cleanup is run really quickly - task = _task ?? TaskHelper.CompletedTask; - Cancel(); - } - await task.ConfigureAwait(false); - } - - public async Task SignalError(Exception ex) - { - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (_stopReason != null) return; - - Cancel(ex); - } - } - public async Task Error(Exception ex) - { - Task task; - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (_stopReason != null) return; - - //Cache the task so we still have something to await if Cleanup is run really quickly - task = _task ?? TaskHelper.CompletedTask; - Cancel(ex); - } - await task.ConfigureAwait(false); - } - private void Cancel(Exception ex = null) - { - var source = _cancelSource; - if (source != null && !source.IsCancellationRequested) - { - if (ex != null) - _stopReason = ExceptionDispatchInfo.Capture(ex); - _cancelSource.Cancel(); - } - } - - /// Throws an exception if one was captured. - public void ThrowException() - { - using (_lock.Lock()) - { - if (!WasStopExpected) - _stopReason?.Throw(); - } - } - public void ClearException() - { - using (_lock.Lock()) - { - _stopReason = null; - WasStopExpected = false; - } - } - } -} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 80a6fb51d..4aa7cd55c 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -2,78 +2,38 @@ "version": "1.0.0-alpha1", "description": "An unofficial .Net API wrapper for the Discord client.", "authors": [ "RogueException" ], - "tags": [ - "discord", - "discordapp" - ], + "tags": [ "discord", "discordapp" ], "projectUrl": "https://github.com/RogueException/Discord.Net", "licenseUrl": "http://opensource.org/licenses/MIT", "repository": { "type": "git", "url": "git://github.com/RogueException/Discord.Net" }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], "compilationOptions": { - "allowUnsafe": true, - "warningsAsErrors": true - }, - - "configurations": { - "TestResponses": { - "compilationOptions": { - "define": [ - "DEBUG", - "TRACE", - "TEST_RESPONSES" - ] - } - } + "allowUnsafe": true }, "dependencies": { - "Newtonsoft.Json": "8.0.1", - "Nito.AsyncEx": "3.0.1" + "Newtonsoft.Json": "8.0.3", + "System.Collections.Concurrent": "4.0.12-rc3-*", + "System.Collections.Immutable": "1.2.0-rc3-*", + "System.IO": "4.1.0-rc3-*", + "System.IO.FileSystem": "4.0.1-rc3-*", + "System.Reflection": "4.1.0-rc3-*", + "System.Runtime": "4.1.0-rc3-*", + "System.Net.Requests": "4.0.11-rc3-*", + "System.Net.Http": "4.0.1-rc3-*", + "System.Net.Sockets": "4.1.0-rc3-*", + "System.Net.WebSockets.Client": "4.0.0-rc3-*", + "System.Text.RegularExpressions": "4.0.12-rc3-*", + "System.Threading": "4.0.11-rc3-*", + "System.Threading.Tasks": "4.0.11-rc3-*" }, "frameworks": { - "dotnet5.4": { - "dependencies": { - "System.Collections": "4.0.11-beta-23516", - "System.Collections.Concurrent": "4.0.11-beta-23516", - "System.Dynamic.Runtime": "4.0.11-beta-23516", - "System.IO.FileSystem": "4.0.1-beta-23516", - "System.IO.Compression": "4.1.0-beta-23516", - "System.Linq": "4.0.1-beta-23516", - "System.Net.Http": "4.0.1-beta-23516", - "System.Net.NameResolution": "4.0.0-beta-23516", - "System.Net.Sockets": "4.1.0-beta-23409", - "System.Net.Requests": "4.0.11-beta-23516", - "System.Net.WebSockets.Client": "4.0.0-beta-23516", - "System.Reflection": "4.1.0-beta-23516", - "System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", - "System.Runtime.InteropServices": "4.0.21-beta-23516", - "System.Runtime.Serialization.Primitives": "4.1.0-beta-23516", - "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", - "System.Text.RegularExpressions": "4.0.11-beta-23516", - "System.Threading": "4.0.11-beta-23516" - } - }, - "net45": { - "frameworkAssemblies": { - "System.Runtime": { - "type": "build", - "version": "" - }, - "System.Threading.Tasks": { - "type": "build", - "version": "" - } - }, - "dependencies": { - "WebSocket4Net": "0.14.1", - "RestSharp": "105.2.3" - } + "netstandard1.4": { + "imports": "dotnet5.5" } } } \ No newline at end of file diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 0c13b7075..2a50610cc 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -8,7 +8,7 @@ Properties Discord.Tests Discord.Net.Tests - v4.5 + v4.6.1 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -56,17 +56,16 @@ - - - {8d71a857-879a-4a10-859e-5ff824ed6688} - Discord.Net - + - + + {c6a50d24-cbd3-4e76-852c-4dca60bbd608} + Discord.Net.Net45 + diff --git a/test/Discord.Net.Tests/Settings.cs b/test/Discord.Net.Tests/Settings.cs deleted file mode 100644 index 5aa37e184..000000000 --- a/test/Discord.Net.Tests/Settings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using System.IO; - -namespace Discord.Tests -{ - internal class Settings - { - private const string path = "../../config.json"; - public static readonly Settings Instance; - static Settings() - { - if (!File.Exists(path)) - throw new FileNotFoundException("config.json is missing, rename config.json.example and add credentials for three separate unused accounts for testing."); - Instance = JsonConvert.DeserializeObject(File.ReadAllText(path)); - } - - public class Account - { - [JsonProperty("email")] - public string Email { get; set; } - [JsonProperty("password")] - public string Password { get; set; } - } - - [JsonProperty("user1")] - public Account User1 { get; set; } - [JsonProperty("user2")] - public Account User2 { get; set; } - [JsonProperty("user3")] - public Account User3 { get; set; } - } -} diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs index 51c045d69..6cb0196d5 100644 --- a/test/Discord.Net.Tests/Tests.cs +++ b/test/Discord.Net.Tests/Tests.cs @@ -7,167 +7,484 @@ using System.Threading.Tasks; namespace Discord.Tests { - //TODO: Tests are massively incomplete and out of date, needing a full rewrite - - [TestClass] - public class Tests - { - private const int EventTimeout = 5000; //Max time in milliseconds to wait for an event response from our test actions - - private static DiscordClient _hostClient, _targetBot, _observerBot; - private static Server _testServer; - private static Channel _testServerChannel; - private static Random _random; - - [ClassInitialize] - public static void Initialize(TestContext testContext) - { - var settings = Settings.Instance; - _random = new Random(); - - _hostClient = new DiscordClient(); - _targetBot = new DiscordClient(); - _observerBot = new DiscordClient(); - - _hostClient.Connect(settings.User1.Email, settings.User1.Password).Wait(); - _targetBot.Connect(settings.User2.Email, settings.User2.Password).Wait(); - _observerBot.Connect(settings.User3.Email, settings.User3.Password).Wait(); - - //Cleanup existing servers - WaitMany( - _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), - _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), - _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave())); - - //Create new server and invite the other bots to it - _testServer = _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First()).Result; - _testServerChannel = _testServer.DefaultChannel; - var invite = _testServer.CreateInvite(60, 2, false, false).Result; - WaitAll( - _targetBot.GetInvite(invite.Code).Result.Accept(), - _observerBot.GetInvite(invite.Code).Result.Accept()); - } - - //Channels - [TestMethod] - public void TestCreateTextChannel() - => TestCreateChannel(ChannelType.Text); - [TestMethod] - public void TestCreateVoiceChannel() - => TestCreateChannel(ChannelType.Voice); - private void TestCreateChannel(ChannelType type) - { - Channel channel = null; - string name = $"#test_{_random.Next()}"; - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testServer.CreateChannel(name.Substring(1), type), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Name == name); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Name == name); - } - - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_NoName() - { - await _testServer.CreateChannel($"", ChannelType.Text); - } - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_NoType() - { - string name = $"#test_{_random.Next()}"; - await _testServer.CreateChannel($"", ChannelType.FromString("")); - } - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_BadType() - { - string name = $"#test_{_random.Next()}"; - await _testServer.CreateChannel($"", ChannelType.FromString("badtype")); - } - - //Messages - [TestMethod] - public void TestSendMessage() - { - string text = $"test_{_random.Next()}"; - AssertEvent( - "MessageCreated event never received", - () => _testServerChannel.SendMessage(text), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.Text == text); - } - - [ClassCleanup] - public static void Cleanup() - { - WaitMany( - _hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, - _targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, - _observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null); - - WaitAll( - _hostClient.Disconnect(), - _targetBot.Disconnect(), - _observerBot.Disconnect()); - } - - // Unit Test Helpers - - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, true); - } - private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, false); - } - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) - { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => - { - if (test != null) - { - result |= test(s, e); - trigger.Set(); + //TODO: Tests are massively incomplete and out of date, needing a full rewrite + + [TestClass] + public class Tests + { + private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions + + private static DiscordSocketClient _hostBot, _targetBot, _observerBot; + private static Guild _testGuild; + private static TextChannel _testGuildChannel; + private static Random _random; + private static PublicInvite _testGuildInvite; + + private static TestContext _context; + + private static string _hostToken; + private static string _observerToken; + private static string _targetToken; + + private static string GetRandomText() + { + lock (_random) + return $"test_{_random.Next()}"; + } + + #region Initialization + + [ClassInitialize] + public static void Initialize(TestContext testContext) + { + _context = testContext; + + _hostToken = Environment.GetEnvironmentVariable("discord-unit-host_token"); + _observerToken = Environment.GetEnvironmentVariable("discord-unit-observer_token"); + _targetToken = Environment.GetEnvironmentVariable("discord-unit-target_token"); + } + + [TestMethod] + [Priority(1)] + public async Task TestInitialize() + { + _context.WriteLine("Initializing."); + + _random = new Random(); + + _hostBot = new DiscordSocketClient(_hostToken); + _targetBot = new DiscordSocketClient(_targetToken); + _observerBot = new DiscordSocketClient(_observerToken); + + await _hostBot.Login(); + + await Task.Delay(3000); + + //Cleanup existing Guilds + (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()); + + //Create new Guild and invite the other bots to it + + _testGuild = await _hostBot.CreateGuild("Discord.Net Testing", _hostBot.GetOptimalVoiceRegion()); + + await Task.Delay(1000); + + PublicInvite invite = await _testGuild.CreateInvite(60, 3, false, false); + _testGuildInvite = invite; + + _context.WriteLine($"Host: {_hostBot.CurrentUser.Username} in {(await _hostBot.GetGuilds()).Count()}"); + } + + [TestMethod] + [Priority(2)] + public async Task TestTokenLogin_Ready() + { + AssertEvent( + "READY never received", + async () => await _observerBot.Login(), + x => _observerBot.Connected += x, + x => _observerBot.Connected -= x, + null, + true); + (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave()); + await _observerBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)); + } + + [TestMethod] + [Priority(2)] + public async Task TestReady() + { + AssertEvent( + "READY never received", + async () => await _targetBot.Login(), + x => _targetBot.Connected += x, + x => _targetBot.Connected -= x, + null, + true); + + (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()); + _testGuildChannel = _testGuild.DefaultChannel; + } + + #endregion + + // Guilds + + #region Guild Tests + + [TestMethod] + [Priority(3)] + public void TestJoinedGuild() + { + AssertEvent( + "Never Got JoinedGuild", + async () => await _targetBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)), + x => _targetBot.JoinedGuild += x, + x => _targetBot.JoinedGuild -= x); + } + + #endregion + + #region Channel Tests + + //Channels + [TestMethod] + public void TestCreateTextChannel() + { + GuildChannel channel = null; + string name = GetRandomText(); + AssertEvent( + "ChannelCreated event never received", + async () => channel = await _testGuild.CreateTextChannel(name), + x => _targetBot.ChannelCreated += x, + x => _targetBot.ChannelCreated -= x, + (s, e) => e.Channel.Id == channel.Id); + + AssertEvent( + "ChannelDestroyed event never received", + async () => await channel.Delete(), + x => _targetBot.ChannelDestroyed += x, + x => _targetBot.ChannelDestroyed -= x, + (s, e) => e.Channel.Id == channel.Id); + } + [TestMethod] + public void TestCreateVoiceChannel() + { + GuildChannel channel = null; + string name = GetRandomText(); + AssertEvent( + "ChannelCreated event never received", + async () => channel = await _testGuild.CreateVoiceChannel(name), + x => _targetBot.ChannelCreated += x, + x => _targetBot.ChannelCreated -= x, + (s, e) => e.Channel.Id == channel.Id); + + AssertEvent( + "ChannelDestroyed event never received", + async () => await channel.Delete(), + x => _targetBot.ChannelDestroyed += x, + x => _targetBot.ChannelDestroyed -= x, + (s, e) => e.Channel.Id == channel.Id); + } + + [TestMethod] + [ExpectedException(typeof(Net.HttpException))] + public async Task TestCreateChannel_NoName() + { + await _testGuild.CreateTextChannel($""); + } + [TestMethod] + public async Task Test_CreateGetChannel() + { + var name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + var get_channel = _testGuild.GetChannel(channel.Id); + Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal."); + } + [TestMethod] + public void TestSendTyping() + { + var channel = _testGuildChannel; + AssertEvent( + "UserUpdated event never fired.", + async () => await channel.TriggerTyping(), + x => _targetBot.UserIsTyping += x, + x => _targetBot.UserIsTyping -= x); + } + [TestMethod] + public void TestEditChannel() + { + var channel = _testGuildChannel; + AssertEvent( + "ChannelUpdated Never Received", + async () => await channel.Modify(x => + { + x.Name = GetRandomText(); + x.Topic = $"topic - {GetRandomText()}"; + x.Position = 26; + }), + x => _targetBot.ChannelUpdated += x, + x => _targetBot.ChannelUpdated -= x); + } + [TestMethod] + public void TestChannelMention() + { + var channel = _testGuildChannel; + Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention."); + } + [TestMethod] + public void TestChannelUserCount() + { + Assert.AreEqual(3, _testGuildChannel.Users.Count(), "Read an incorrect number of users in a channel"); + } + + #endregion + + #region Message Tests + + //Messages + [TestMethod] + public async Task TestMessageEvents() + { + string name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + _context.WriteLine($"Channel Name: {channel.Name} / {channel.Guild.Name}"); + string text = GetRandomText(); + Message message = null; + AssertEvent( + "MessageCreated event never received", + async () => message = await channel.SendMessage(text), + x => _targetBot.MessageReceived += x, + x => _targetBot.MessageReceived -= x, + (s, e) => e.Message.Text == text); + + AssertEvent( + "MessageUpdated event never received", + async () => await message.Modify(x => + { + x.Text = text + " updated"; + }), + x => _targetBot.MessageUpdated += x, + x => _targetBot.MessageUpdated -= x, + (s, e) => e.Before.Text == text && e.After.Text == text + " updated"); + + AssertEvent( + "MessageDeleted event never received", + async () => await message.Delete(), + x => _targetBot.MessageDeleted += x, + x => _targetBot.MessageDeleted -= x, + (s, e) => e.Message.Id == message.Id); + } + [TestMethod] + public async Task TestDownloadMessages() + { + string name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + for (var i = 0; i < 10; i++) await channel.SendMessage(GetRandomText()); + while (channel.Discord.MessageQueue.Count > 0) await Task.Delay(100); + var messages = await channel.GetMessages(10); + Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); + } + [TestMethod] + public async Task TestSendTTSMessage() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + AssertEvent( + "MessageCreated event never fired", + async () => await channel.SendMessage(GetRandomText(), true), + x => _targetBot.MessageReceived += x, + x => _targetBot.MessageReceived -= x, + (s, e) => e.Message.IsTTS); + } + + #endregion + + #region User Tests + + [TestMethod] + public async Task TestUserMentions() + { + var user = (await _targetBot.GetGuild(_testGuild.Id)).CurrentUser; + Assert.AreEqual($"<@{user.Id}>", user.Mention); + } + [TestMethod] + public void TestUserEdit() + { + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + AssertEvent( + "UserUpdated never fired", + async () => await user.Modify(true, true, null, null), + x => _targetBot.UserUpdated += x, + x => _targetBot.UserUpdated -= x); + } + [TestMethod] + public void TestEditSelf() + { + throw new NotImplementedException(); + /*var name = RandomText + AssertEvent( + "UserUpdated never fired", + async () => await _targetBot.CurrentUser.Modify(TargetPassword, name), + x => _obGuildBot.UserUpdated += x, + x => _obGuildBot.UserUpdated -= x, + (s, e) => e.After.Username == name);*/ + } + [TestMethod] + public void TestSetStatus() + { + AssertEvent( + "UserUpdated never fired", + async () => await SetStatus(_targetBot, UserStatus.Idle), + x => _observerBot.UserUpdated += x, + x => _observerBot.UserUpdated -= x, + (s, e) => e.After.Status == UserStatus.Idle); + } + private async Task SetStatus(DiscordClient _client, UserStatus status) + { + throw new NotImplementedException(); + /*_client.SetStatus(status); + await Task.Delay(50);*/ + } + [TestMethod] + public void TestSetGame() + { + AssertEvent( + "UserUpdated never fired", + async () => await SetGame(_targetBot, "test game"), + x => _observerBot.UserUpdated += x, + x => _observerBot.UserUpdated -= x, + (s, e) => _targetBot.CurrentUser.CurrentGame == "test game"); + + } + private async Task SetGame(DiscordClient _client, string game) + { + throw new NotImplementedException(); + //_client.SetGame(game); + //await Task.Delay(5); + } + + #endregion + + #region Permission Tests + + // Permissions + [TestMethod] + public async Task Test_AddGet_PermissionsRule() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.UpdatePermissionOverwrite(user, perms); + var resultPerms = channel.GetPermissionOverwrite(user); + Assert.IsNotNull(resultPerms, "Perms retrieved from Guild were null."); + } + [TestMethod] + public async Task Test_AddRemove_PermissionsRule() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.UpdatePermissionOverwrite(user, perms); + await channel.RemovePermissionOverwrite(user); + await Task.Delay(200); + Assert.AreEqual(PermValue.Inherit, channel.GetPermissionOverwrite(user)?.SendMessages); + } + [TestMethod] + public async Task Test_Permissions_Event() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + AssertEvent + ("ChannelUpdatedEvent never fired.", + async () => await channel.UpdatePermissionOverwrite(user, perms), + x => _targetBot.ChannelUpdated += x, + x => _targetBot.ChannelUpdated -= x, + (s, e) => e.Channel == channel && (e.After as GuildChannel).PermissionOverwrites.Count() != (e.Before as GuildChannel).PermissionOverwrites.Count()); + } + [TestMethod] + [ExpectedException(typeof(Net.HttpException))] + public async Task Test_Affect_Permissions_Invalid_Channel() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.Delete(); + await channel.UpdatePermissionOverwrite(user, perms); + } + + #endregion + + + [ClassCleanup] + public static async Task Cleanup() + { + WaitMany( + (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()), + (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()), + (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave())); + + WaitAll( + _hostBot.Disconnect(), + _targetBot.Disconnect(), + _observerBot.Disconnect()); + } + + #region Helpers + + // Task Helpers + + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) + { + AssertEvent(msg, action, addEvent, removeEvent, test, true); + } + private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) + { + AssertEvent(msg, action, addEvent, removeEvent, test, false); + } + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) + { + ManualResetEventSlim trigger = new ManualResetEventSlim(false); + bool result = false; + + EventHandler handler = (s, e) => + { + if (test != null) + { + result |= test(s, e); + trigger.Set(); + } + else + result = true; + }; + + addEvent(handler); + var task = action(); + trigger.Wait(EventTimeout); + task.Wait(); + removeEvent(handler); + + Assert.AreEqual(assertTrue, result, msg); + } + + private static void AssertEvent(string msg, Func action, Action addEvent, Action removeEvent, Func test, bool assertTrue) + { + ManualResetEventSlim trigger = new ManualResetEventSlim(false); + bool result = false; + + EventHandler handler = (s, e) => + { + if (test != null) + { + result |= test(s); + trigger.Set(); } - else - result = true; - }; - - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); - } - - private static void WaitAll(params Task[] tasks) - { - Task.WaitAll(tasks); - } - private static void WaitAll(IEnumerable tasks) - { - Task.WaitAll(tasks.ToArray()); - } - private static void WaitMany(params IEnumerable[] tasks) - { - Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); - } - } + else + result = true; + }; + + addEvent(handler); + var task = action(); + trigger.Wait(EventTimeout); + task.Wait(); + removeEvent(handler); + + Assert.AreEqual(assertTrue, result, msg); + } + + private static void WaitAll(params Task[] tasks) + { + Task.WaitAll(tasks); + } + private static void WaitAll(IEnumerable tasks) + { + Task.WaitAll(tasks.ToArray()); + } + private static void WaitMany(params IEnumerable[] tasks) + { + Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); + } + + #endregion + } }